diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 0e075442..8a9bd7e1 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -12,110 +12,84 @@ on: - master jobs: - linter: - name: Linter on C & Python code - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Python dependency - run: pip install flake8 - - name: Lint C code - uses: DoozyX/clang-format-lint-action@v0.13 - with: - source: "src tests" - extensions: "c,h" - clangFormatVersion: 11 - - name: Lint Python code - run: find . -type f -name '*.py' -exec flake8 --max-line-length=120 '{}' '+' - - misspell: - name: Check misspellings - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Check misspellings - uses: codespell-project/actions-codespell@de089481bd65b71b4d02e34ffb3566b6d189333e - with: - builtin: clear,rare - check_filenames: true - ignore_words_file: .codespell-ignore - skip: ./speculos/api/static/swagger/swagger-ui.css,./speculos/api/static/swagger/swagger-ui-bundle.js,./speculos/api/static/swagger/swagger-ui-standalone-preset.js - coverage: name: Code coverage runs-on: ubuntu-latest - needs: [linter, misspell] container: image: docker://ghcr.io/ledgerhq/speculos-builder:latest steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Rebuild with code coverage instrumentation - env: - CTEST_OUTPUT_ON_FAILURE: 1 - RNG_SEED: 0 - run: | - cmake -Bbuild -H. -DPRECOMPILED_DEPENDENCIES_DIR=/install -DWITH_VNC=1 -DCODE_COVERAGE=ON - make -C build clean - make -C build - make -C build test - python3 -m pip install pytest-cov - python3 -m pytest --cov=speculos --cov-report=xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - name: codecov-speculos + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build with code coverage instrumentation + env: + CTEST_OUTPUT_ON_FAILURE: 1 + RNG_SEED: 0 + run: | + cmake -Bbuild -H. -DPRECOMPILED_DEPENDENCIES_DIR=/install -DWITH_VNC=1 -DCODE_COVERAGE=ON + make -C build clean + make -C build + make -C build test + pip install pytest-cov + pip install . + PYTHONPATH=. pytest --cov=speculos --cov-report=xml + - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + name: codecov-speculos build: name: Clone, build, test runs-on: ubuntu-latest - needs: [linter, misspell] - permissions: - packages: write - + strategy: + matrix: + python_version: ['3.8', '3.9', '3.10', '3.11'] # Use https://ghcr.io/ledgerhq/speculos-builder which has all the required # dependencies container: image: docker://ghcr.io/ledgerhq/speculos-builder:latest steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Build - run: | - cmake -Bbuild -H. -DPRECOMPILED_DEPENDENCIES_DIR=/install -DWITH_VNC=1 - make -C build - - - name: Test - env: - CTEST_OUTPUT_ON_FAILURE: 1 - run: | - make -C build/ test - python3 -m pytest + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python version + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + + - name: Build and install package + run: | + cmake -Bbuild -H. -DPRECOMPILED_DEPENDENCIES_DIR=/install -DWITH_VNC=1 + make -C build + pip install pytest + pip install . + + - name: Test + env: + CTEST_OUTPUT_ON_FAILURE: 1 + run: | + make -C build/ test + pytest package_python: name: Build and deploy Speculos Python Package runs-on: ubuntu-latest - needs: [build, coverage] + needs: [build] container: image: docker://ghcr.io/ledgerhq/speculos-builder:latest steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Use pip to install Speculos in a virtual environment run: | @@ -136,46 +110,26 @@ jobs: ./venv-build/bin/python -m build ./venv-build/bin/python -m twine check dist/* - - name: Display current status - shell: bash - run: | - echo "Current status is:" - if [[ ${{ github.ref }} == "refs/tags/"* ]]; \ - then \ - echo "- Triggered from tag, will be deployed on pypi.org"; \ - else \ - echo "- Not triggered from tag, will be deployed on test.pypi.org"; \ - fi - echo "- Tag version: ${{ env.TAG_VERSION }}" - - - name: Check version against CHANGELOG - if: startsWith(github.ref, 'refs/tags/') - shell: bash - run: | - CHANGELOG_VERSION=$(grep -Po '(?<=## \[)(\d\.)+[^\]]' CHANGELOG.md | head -n 1) - if [ "${{ env.TAG_VERSION }}" == "${CHANGELOG_VERSION}" ]; \ - then \ - exit 0; \ - else \ - echo "Tag '${{ env.TAG_VERSION }}' and CHANGELOG '${CHANGELOG_VERSION}' versions mismatch!"; \ - exit 1; \ - fi - - # - name: Publish Python package on pypi.org - # if: success() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - # run: ./venv-build/bin/python -m twine upload dist/* - # env: - # TWINE_USERNAME: __token__ - # TWINE_PASSWORD: ${{ secrets.PYPI_PUBLIC_API_TOKEN }} - # TWINE_NON_INTERACTIVE: 1 - - # - name: Publish Python package on test.pypi.org - # if: success() && github.event_name == 'push' - # run: ./venv-build/bin/python -m twine upload --repository testpypi dist/* - # env: - # TWINE_USERNAME: __token__ - # TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PUBLIC_API_TOKEN }} - # TWINE_NON_INTERACTIVE: 1 + - name: Check version against CHANGELOG + if: startsWith(github.ref, 'refs/tags/') + shell: bash + run: | + CHANGELOG_VERSION=$(grep -Po '(?<=## \[)(\d+\.)+[^\]]' CHANGELOG.md | head -n 1) + if [ "${{ env.TAG_VERSION }}" == "${CHANGELOG_VERSION}" ]; \ + then \ + exit 0; \ + else \ + echo "Tag '${{ env.TAG_VERSION }}' and CHANGELOG '${CHANGELOG_VERSION}' versions mismatch!"; \ + exit 1; \ + fi + + - name: Publish Python package on pypi.org + if: success() && github.event_name == 'push' + run: ./venv-build/bin/python -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PUBLIC_API_TOKEN }} + TWINE_NON_INTERACTIVE: 1 package_and_test_docker: name: Build and test the Speculos docker @@ -192,18 +146,18 @@ jobs: if: | github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) - needs: [build, coverage, package_and_test_docker] + needs: [build] steps: - - name: Clone - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Build and publish to GitHub Packages - uses: docker/build-push-action@v1 - with: - repository: blooo-io/speculos - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - tag_with_sha: true - tags: latest + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build and publish to GitHub Packages + uses: docker/build-push-action@v1 + with: + repository: ledgerhq/speculos + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + tag_with_sha: true + tags: latest diff --git a/.github/workflows/fast-checks.yml b/.github/workflows/fast-checks.yml new file mode 100644 index 00000000..712b34e1 --- /dev/null +++ b/.github/workflows/fast-checks.yml @@ -0,0 +1,74 @@ +name: Fast checks + +on: + workflow_dispatch: + push: + branches: + - master + - develop + pull_request: + +jobs: + linter-python: + name: Linter on Python code + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Python dependency + run: pip install flake8 flake8-pyproject + - name: Lint Python code + run: flake8 speculos* setup.py + + linter-c: + name: Linter on C code + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Lint C code + uses: DoozyX/clang-format-lint-action@v0.16.1 + with: + source: 'src tests' + extensions: 'c,h' + clangFormatVersion: 11 + + mypy: + name: Type checking + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + - run: pip install mypy types-requests types-setuptools PyQt5-stubs + - name: Mypy type checking + run: mypy speculos + + bandit: + name: Security checking + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + - run: pip install bandit + - name: Bandit security checking + run: bandit -r speculos -ll || echo 0 + + misspell: + name: Check misspellings + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check misspellings + uses: codespell-project/actions-codespell@v1 + with: + builtin: clear,rare + check_filenames: true + ignore_words_file: .codespell-ignore + skip: ./speculos/api/static/swagger/swagger-ui.css,./speculos/api/static/swagger/swagger-ui-bundle.js,./speculos/api/static/swagger/swagger-ui-standalone-preset.js,./speculos/fonts diff --git a/.github/workflows/force-rebase.yml b/.github/workflows/force-rebase.yml new file mode 100644 index 00000000..9337156e --- /dev/null +++ b/.github/workflows/force-rebase.yml @@ -0,0 +1,31 @@ +name: Force rebased + +on: [pull_request] + +jobs: + force-rebase: + runs-on: ubuntu-latest + steps: + + - name: 'PR commits + 1' + id: pr_commits + run: echo "pr_fetch_depth=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_OUTPUT}" + + - name: 'Checkout PR branch and all PR commits' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: ${{ steps.pr_commits.outputs.pr_fetch_depth }} + + - name: Check if PR branch is rebased on target branch + shell: bash + run: | + git merge-base --is-ancestor ${{ github.event.pull_request.base.sha }} HEAD + + - name: Check if PR branch contains merge commits + shell: bash + run: | + merges=$(git log --oneline HEAD~${{ github.event.pull_request.commits }}...HEAD --merges ); \ + echo "--- Merges ---"; \ + echo ${merges}; \ + [[ -z "${merges}" ]] diff --git a/.github/workflows/reusable_ragger_tests_latest_speculos.yml b/.github/workflows/reusable_ragger_tests_latest_speculos.yml index 113fa60a..a33b8225 100644 --- a/.github/workflows/reusable_ragger_tests_latest_speculos.yml +++ b/.github/workflows/reusable_ragger_tests_latest_speculos.yml @@ -28,11 +28,39 @@ jobs: app_branch_name: ${{ inputs.app_branch_name }} upload_app_binaries_artifact: "compiled_app_binaries" + build_docker_image: + name: Build Speculos Docker image + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Clone + uses: actions/checkout@v4 + with: + ref: ${{ inputs.speculos_app_branch_name }} + submodules: recursive + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build the Speculos docker + uses: docker/build-push-action@v4 + with: + push: false + tags: ledgerhq/speculos:test + context: . + outputs: type=docker,dest=/tmp/speculos_image.tar + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: speculos_image + path: /tmp/speculos_image.tar + ragger_tests: name: Functional tests with Ragger runs-on: ubuntu-latest - container: - image: docker://ghcr.io/ledgerhq/speculos-builder:latest + needs: [build_docker_image, build_application] strategy: fail-fast: false matrix: @@ -43,21 +71,15 @@ jobs: - device: stax steps: - name: Clone - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ledgerHQ/speculos ref: ${{ inputs.speculos_app_branch_name }} submodules: recursive fetch-depth: 0 - - name: Build the Speculos docker - uses: docker/build-push-action@v1 - with: - push: false - tags: test - - name: Clone - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ inputs.app_repository }} ref: ${{ inputs.app_branch_name }} @@ -65,6 +87,20 @@ jobs: submodules: recursive fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: speculos_image + path: /tmp + + - name: Load image + run: | + docker load --input /tmp/speculos_image.tar + docker image ls -a + - name: Download app binaries uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/speculos-builder.yml b/.github/workflows/speculos-builder.yml index 81b94172..6077fc4f 100644 --- a/.github/workflows/speculos-builder.yml +++ b/.github/workflows/speculos-builder.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build and push speculos-builder to GitHub Packages uses: docker/build-push-action@v1 diff --git a/.gitignore b/.gitignore index b41886a9..f5ea6793 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ apps/*vault* build/ core +TAGS /speculos/resources/launcher /speculos/resources/vnc_server @@ -16,3 +17,4 @@ __version__.py *.pyc dist/ *egg-info +.coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3d0f54..71f52004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,131 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.1] - 2-24-02-26 + +### Fixed + +- Specific `cache` mechanism for Python3.8 (`functools.cache` does not exists yet) + +## [0.7.0] - 2024-02-26 + +### Changed +- Significative performance improvement on display/snapshot management +- Simplified HTTP API thread management + +## [0.6.0] - 2024-02-21 + +### Added +- Add support for API_LEVEL_15 for Stax + +## [0.5.1] - 2024-02-15 + +### Added +- Add possibility to set up a timeout for APDU exchange with default value to 5min + +## [0.5.0] - 2024-01-11 + +### Added +- Attestation key or user private keys can now be configured with the new `--attestation-key` + and `--user-private-key` arguments (or `ATTESTATION_PRIVATE_KEY` and `USER_PRIVATE_KEY` through + environment variables). User certificates are correctly calculated, signed from the user private + keys and the attestation key. + +### Changed +- Seed, RNG, application name and version are now fetched from the environment when Speculos starts + then stored internally for further use, rather than fetched when needed during runtime. This + avoids several Speculos instances from messing up with each other's environment variables. + +## [0.4.1] - 2023-12-19 + +### Fixed +- CX: Fix AES implementation on NanoS + +## [0.4.0] - 2023-12-04 + +### Fixed +- bolos/os_bip32.c: Improve syscall emulation + +### Added +- API_LEVEL: Add support for API_LEVEL_14 for Ledger Stax + +## [0.3.5] - 2023-11-10 + +### Fixed +- CX: Update AES implementation to be compatible with API levels >= 12 + +## [0.3.4] - 2023-11-07 + +### Features + +- Implement cx_bn_gf2_n_mul() + +### Miscellaneous Tasks + +- Add missing `binutils-arm-none-eabi` package + +### README + +- Update Limitations section + +## [0.3.3] - 2023-10-26 + +### Fixed +- Launcher: Fix missing RAM relocation emulation on NanoX, NanoSP and Stax + +## [0.3.2] - 2023-09-28 + +### Fixed +- API: the API thread is asked to stop when Speculos exits + +## [0.3.1] - 2023-09-28 + +### Fixed +- OCR: Prevent null dereference when expected font is not in ELF file + +## [0.3.0] - 2023-09-11 + +### Added +- API_LEVEL: Add support for API_LEVEL_13 for corresponding device + +## [0.2.10] - 2023-09-01 + +### Changed +- OCR: Apps using unified SDK don't use OCR anymore. Font info is parsed from .elf file. + +## [0.2.9] - 2023-08-31 + +### Fixed +- Seproxyhal: default status_sent value upon app launch is unset. + +## [0.2.8] - 2023-07-31 + +### Changed +- OCR: Change Stax OCR method. Don't use Tesseract anymore. +- CI: Remove CI job dependency to allow deployment if wanted + +### Added +- API_LEVEL: Add support for API_LEVEL_12 for corresponding device + +## [0.2.7] - 2023-06-30 + +### Fixed +- Seproxyhal: Fix SeProxyHal emulation + +## [0.2.6] - 2023-06-26 + +### Fixed +- Seproxyhal: Fix SeProxyHal issue when on LNSP / LNX and HAVE_PRINTF is enabled + +## [0.2.5] - 2023-06-21 + +### Added +- API: Add a ticker/ endpoint to allow control of the ticks sent to the app + +### Fixed +- OCR: Fix OCR on NanoX and NanoSP based on API_LEVEL_5 and upper +- Seproxyhal: Fix SeProxyHal emulation to match real devices behavior + ## [0.2.4] - 2023-06-13 ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 26e82329..0050d705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,7 +85,7 @@ else() openssl URL https://www.openssl.org/source/openssl-1.1.1k.tar.gz URL_HASH SHA256=892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5 - CONFIGURE_COMMAND ./Configure "CC=${CMAKE_C_COMPILER}" "CFLAGS=${OPENSSL_CFLAGS}" no-afalgeng no-aria no-asan no-asm no-async no-autoalginit no-autoerrinit no-autoload-config no-bf no-buildtest-c++ no-camellia no-capieng no-cast no-chacha no-cmac no-cms no-comp no-crypto-mdebug no-crypto-mdebug-backtrace no-ct no-deprecated no-des no-devcryptoeng no-dgram no-dh no-dsa no-dso no-dtls no-ec2m no-ecdh no-egd no-engine no-err no-external-tests no-filenames no-fuzz-afl no-fuzz-libfuzzer no-gost no-heartbeats no-hw no-idea no-makedepend no-md2 no-md4 no-mdc2 no-msan no-multiblock no-nextprotoneg no-ocb no-ocsp no-pinshared no-poly1305 no-posix-io no-psk no-rc2 no-rc4 no-rc5 no-rdrand no-rfc3779 no-scrypt no-sctp no-seed no-shared no-siphash no-sm2 no-sm3 no-sm4 no-sock no-srp no-srtp no-sse2 no-ssl no-ssl3-method no-ssl-trace no-stdio no-tests no-threads no-tls no-ts no-ubsan no-ui-console no-unit-test no-whirlpool no-zlib no-zlib-dynamic linux-armv4 --prefix=${INSTALL_DIR} + CONFIGURE_COMMAND ./Configure "CC=${CMAKE_C_COMPILER}" "CFLAGS=${OPENSSL_CFLAGS}" no-afalgeng no-aria no-asan no-asm no-async no-autoalginit no-autoerrinit no-autoload-config no-bf no-buildtest-c++ no-camellia no-capieng no-cast no-chacha no-cmac no-cms no-comp no-crypto-mdebug no-crypto-mdebug-backtrace no-ct no-deprecated no-des no-devcryptoeng no-dgram no-dh no-dsa no-dso no-dtls no-ecdh no-egd no-engine no-err no-external-tests no-filenames no-fuzz-afl no-fuzz-libfuzzer no-gost no-heartbeats no-hw no-idea no-makedepend no-md2 no-md4 no-mdc2 no-msan no-multiblock no-nextprotoneg no-ocb no-ocsp no-pinshared no-poly1305 no-posix-io no-psk no-rc2 no-rc4 no-rc5 no-rdrand no-rfc3779 no-scrypt no-sctp no-seed no-shared no-siphash no-sm2 no-sm3 no-sm4 no-sock no-srp no-srtp no-sse2 no-ssl no-ssl3-method no-ssl-trace no-stdio no-tests no-threads no-tls no-ts no-ubsan no-ui-console no-unit-test no-whirlpool no-zlib no-zlib-dynamic linux-armv4 --prefix=${INSTALL_DIR} BUILD_COMMAND make INSTALL_COMMAND make install_sw BUILD_IN_SOURCE 1 @@ -136,7 +136,7 @@ link_libraries(ssl crypto dl blst) add_subdirectory(src) if (BUILD_TESTING) - add_subdirectory(tests/syscalls) + add_subdirectory(tests/c/) endif() add_custom_target( diff --git a/Dockerfile b/Dockerfile index b06ea99f..1ab457b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,15 +24,13 @@ COPY --from=builder /speculos/speculos/resources/ /speculos/speculos/resources/ RUN pip install --upgrade pip pipenv RUN pipenv install --deploy --system -RUN pip install pytesseract RUN apt-get update && apt-get install -qy \ qemu-user-static \ libvncserver-dev \ gdb-multiarch \ - tesseract-ocr \ - libtesseract-dev \ + binutils-arm-none-eabi \ && apt-get clean RUN apt-get clean && rm -rf /var/lib/apt/lists/ diff --git a/MANIFEST.in b/MANIFEST.in index ebfbe7f3..7ed3f683 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,7 +10,6 @@ include speculos/api/static/swagger/*.json include speculos/api/static/swagger/*.md include speculos/cxlib/*.elf include speculos/fonts/*.bin -include speculos/fonts/*.json include speculos/mcu/resources/*.schema include speculos/resources/launcher include speculos/resources/vnc_server diff --git a/Pipfile b/Pipfile index fd5ed42b..9d2de6ff 100644 --- a/Pipfile +++ b/Pipfile @@ -6,15 +6,15 @@ verify_ssl = true [dev-packages] [packages] -jsonschema = "*" -pyelftools = "*" -mnemonic = "*" -construct = "*" -flask="*" -flask-restful="*" -pillow="*" -requests = "*" -pytest = "*" +construct = ">=2.10.56,<3.0.0" +flask = ">=2.0.0,<3.0.0" +flask-restful = ">=0.3.9,<1.0" +jsonschema = ">=3.2.0,<4.18.0" +mnemonic = ">=0.19,<1.0" +pillow = ">=8.0.0,<11.0.0" +pyelftools = ">=0.27,<1.0" +pyqt5 = ">=5.15.2,<6.0.0" +requests = ">=2.25.1,<3.0.0" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index cb5cd3a2..a9c57b39 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5c4a2f0118edcfb331ba1f7c1f74872a446ab8fe5f64bd822054129c17a1589d" + "sha256": "1d2d56fb3692fb2ee39ed3517d4534cfb462c4beee4f8d22fb0f844c55b79b28" }, "pipfile-spec": 6, "requires": { @@ -25,82 +25,165 @@ }, "attrs": { "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "blinker": { + "hashes": [ + "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", + "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" + ], + "markers": "python_version >= '3.8'", + "version": "==1.7.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2024.2.2" }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.7" }, "construct": { "hashes": [ - "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45" + "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", + "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30" ], "index": "pypi", - "version": "==2.10.68" + "markers": "python_version >= '3.6'", + "version": "==2.10.70" }, "flask": { "hashes": [ - "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", - "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" + "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", + "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b" ], "index": "pypi", - "version": "==2.2.2" + "markers": "python_version >= '3.8'", + "version": "==2.3.3" }, "flask-restful": { "hashes": [ - "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2", - "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e" + "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b", + "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37" ], "index": "pypi", - "version": "==0.3.9" + "version": "==0.3.10" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "importlib-metadata": { - "hashes": [ - "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", - "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" - ], - "markers": "python_version < '3.10'", - "version": "==6.0.0" - }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" + "version": "==3.6" }, "itsdangerous": { "hashes": [ @@ -112,238 +195,281 @@ }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.3" }, "jsonschema": { "hashes": [ - "sha256:21f4979391bdceb044e502fd8e79e738c0cdfbdc8773f9a49b5769461e82fe1e", - "sha256:2df0fab225abb3b41967bb3a46fd37dc74b1536b5296d0b1c2078cd072adf0f7" + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" ], "index": "pypi", - "version": "==4.15.0" + "markers": "python_version >= '3.7'", + "version": "==4.17.3" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.5" }, "mnemonic": { "hashes": [ - "sha256:7c6fb5639d779388027a77944680aee4870f0fcd09b1e42a5525ee2ce4c625f6", - "sha256:acd2168872d0379e7a10873bb3e12bf6c91b35de758135c4fbd1015ef18fafc5" + "sha256:1fe496356820984f45559b1540c80ff10de448368929b9c60a2b55744cc88acf", + "sha256:72dc9de16ec5ef47287237b9b6943da11647a03fe7cf1f139fc3d7c4a7439288" ], "index": "pypi", - "version": "==0.20" - }, - "packaging": { - "hashes": [ - "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", - "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" - ], - "markers": "python_version >= '3.7'", - "version": "==23.0" + "markers": "python_full_version >= '3.8.1'", + "version": "==0.21" }, "pillow": { "hashes": [ - "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", - "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8", - "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65", - "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2", - "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627", - "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07", - "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef", - "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535", - "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c", - "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc", - "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3", - "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1", - "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c", - "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa", - "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32", - "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502", - "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4", - "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f", - "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812", - "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636", - "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20", - "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c", - "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91", - "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe", - "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b", - "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad", - "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9", - "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72", - "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4", - "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de", - "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29", - "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee", - "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c", - "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7", - "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11", - "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c", - "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c", - "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448", - "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b", - "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20", - "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228", - "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd", - "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699", - "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b", - "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2", - "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4", - "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c", - "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f", - "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2", - "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c", - "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3", - "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193", - "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48", - "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02", - "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8", - "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e", - "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f", - "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b", - "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74", - "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb", - "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0" + "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", + "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", + "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", + "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", + "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", + "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", + "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", + "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", + "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", + "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", + "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", + "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", + "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", + "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", + "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", + "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", + "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", + "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", + "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", + "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", + "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", + "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", + "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", + "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", + "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", + "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", + "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", + "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", + "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", + "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", + "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", + "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", + "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", + "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", + "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", + "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", + "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", + "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", + "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", + "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", + "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", + "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", + "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", + "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", + "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", + "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", + "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", + "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", + "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", + "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", + "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", + "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", + "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", + "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", + "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", + "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", + "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", + "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", + "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", + "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", + "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", + "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", + "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", + "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", + "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", + "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", + "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", + "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], "index": "pypi", - "version": "==9.3.0" + "markers": "python_version >= '3.8'", + "version": "==10.2.0" }, - "pluggy": { + "pyelftools": { "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + "sha256:2fc92b0d534f8b081f58c7c370967379123d8e00984deb53c209364efd575b40", + "sha256:544c3440eddb9a0dce70b6611de0b28163d71def759d2ed57a0d00118fc5da86" ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" + "index": "pypi", + "version": "==0.30" }, - "py": { + "pyqt5": { "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + "sha256:501355f327e9a2c38db0428e1a236d25ebcb99304cd6e668c05d1188d514adec", + "sha256:862cea3be95b4b0a2b9678003b3a18edf7bd5eafd673860f58820f246d4bf616", + "sha256:93288d62ebd47b1933d80c27f5d43c7c435307b84d480af689cef2474e87e4c8", + "sha256:b89478d16d4118664ff58ed609e0a804d002703c9420118de7e4e70fa1cb5486", + "sha256:d46b7804b1b10a4ff91753f8113e5b5580d2b4462f3226288e2d84497334898a", + "sha256:ff99b4f91aa8eb60510d5889faad07116d3340041916e46c07d519f7cad344e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==5.15.10" }, - "pyelftools": { + "pyqt5-qt5": { "hashes": [ - "sha256:519f38cf412f073b2d7393aa4682b0190fa901f7c3fa0bff2b82d537690c7fc1", - "sha256:ec761596aafa16e282a31de188737e5485552469ac63b60cfcccf22263fd24ff" + "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", + "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962", + "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", + "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327" ], - "index": "pypi", - "version": "==0.29" + "version": "==5.15.2" }, - "pyrsistent": { + "pyqt5-sip": { "hashes": [ - "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", - "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", - "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", - "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", - "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", - "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", - "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", - "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", - "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", - "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", - "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", - "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", - "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", - "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", - "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", - "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", - "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", - "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", - "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", - "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", - "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", - "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", - "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", - "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", - "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", - "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", - "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" + "sha256:0f85fb633a522f04e48008de49dce1ff1d947011b48885b8428838973fbca412", + "sha256:108a15f603e1886988c4b0d9d41cb74c9f9815bf05cefc843d559e8c298a10ce", + "sha256:1c8371682f77852256f1f2d38c41e2e684029f43330f0635870895ab01c02f6c", + "sha256:205cd449d08a2b024a468fb6100cd7ed03e946b4f49706f508944006f955ae1a", + "sha256:29fa9cc964517c9fc3f94f072b9a2aeef4e7a2eda1879cb835d9e06971161cdf", + "sha256:3188a06956aef86f604fb0d14421a110fad70d2a9e943dbacbfc3303f651dade", + "sha256:3a4498f3b1b15f43f5d12963accdce0fd652b0bcaae6baf8008663365827444c", + "sha256:5338773bbaedaa4f16a73c142fb23cc18c327be6c338813af70260b756c7bc92", + "sha256:6e4ac714252370ca037c7d609da92388057165edd4f94e63354f6d65c3ed9d53", + "sha256:773731b1b5ab1a7cf5621249f2379c95e3d2905e9bd96ff3611b119586daa876", + "sha256:7f321daf84b9c9dbca61b80e1ef37bdaffc0e93312edae2cd7da25b953971d91", + "sha256:7fe3375b508c5bc657d73b9896bba8a768791f1f426c68053311b046bcebdddf", + "sha256:96414c93f3d33963887cf562d50d88b955121fbfd73f937c8eca46643e77bf61", + "sha256:9a8cdd6cb66adcbe5c941723ed1544eba05cf19b6c961851b58ccdae1c894afb", + "sha256:9b984c2620a7a7eaf049221b09ae50a345317add2624c706c7d2e9e6632a9587", + "sha256:a7e3623b2c743753625c4650ec7696362a37fb36433b61824cf257f6d3d43cca", + "sha256:bbc7cd498bf19e0862097be1ad2243e824dea56726f00c11cff1b547c2d31d01", + "sha256:d5032da3fff62da055104926ffe76fd6044c1221f8ad35bb60804bcb422fe866", + "sha256:db228cd737f5cbfc66a3c3e50042140cb80b30b52edc5756dbbaa2346ec73137", + "sha256:ec60162e034c42fb99859206d62b83b74f987d58937b3a82bdc07b5c3d190dec", + "sha256:fb4a5271fa3f6bc2feb303269a837a95a6d8dd16be553aa40e530de7fb81bfdf" ], "markers": "python_version >= '3.7'", - "version": "==0.19.3" + "version": "==12.13.0" }, - "pytest": { + "pyrsistent": { "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" ], - "index": "pypi", - "version": "==7.1.2" + "markers": "python_version >= '3.8'", + "version": "==0.20.0" }, "pytz": { "hashes": [ - "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", - "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" ], - "version": "==2022.7.1" + "version": "==2024.1" }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.28.1" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" }, "six": { "hashes": [ @@ -353,37 +479,21 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", + "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "werkzeug": { "hashes": [ - "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", - "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" + "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", + "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" ], - "index": "pypi", - "version": "==2.2.3" - }, - "zipp": { - "hashes": [ - "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", - "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" - ], - "markers": "python_version >= '3.7'", - "version": "==3.13.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.1" } }, "develop": {} diff --git a/README.md b/README.md index b4a3e812..1f471eea 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,43 @@ being merged to `master`: ## Limitations -The emulator handles only a few syscalls made by common apps; for instance, +There is absolutely no guarantee that apps will have the same behavior on +hardware devices and Speculos, though the differences are limited. + +### Syscalls + +The emulator handles only a few syscalls made by common apps. For instance, syscalls related to app install, firmware update or OS info can't be implemented. -There is absolutely no guarantee that apps will have the same behavior on -hardware devices and Speculos: +Invalid syscall parameters might throw an exception on a real device while +being ignored on Speculos. +Notably, this is the case for application allowed derivation path and curve and +application settings flags which are enforced by the device OS, but ignored by +Speculos. + +### Memory alignment + +Attempts to perform unaligned accesses when not allowed (eg. dereferencing a +misaligned pointer) will cause an alignment fault on a Ledger Nano S device but +not on Speculos. Note that such unaligned accesses are supported by other +Ledger devices. + +Following code crashes on LNS device, but not on Speculos nor on other devices. +``` +uint8_t buffer[20]; +for (int i = 0; i < 20; i++) { + buffer[i] = i; +} +uint32_t display_value = *((uint32_t*) (buffer + 1)); +PRINTF("display_value: %d\n", display_value); +``` + +### Watchdog -- Invalid syscall parameters might throw an exception on a real device while - being ignored on Speculos. -- Attempts to perform unaligned accesses when not allowed (eg. dereferencing a - misaligned pointer) will cause an alignment fault on a hardware device. +NanoX and Stax devices use an internal watchdog enforcing usage of regular +calls to `io_seproxyhal_io_heartbeat();`. This watchdog is not emulated on +Speculos. ## Security diff --git a/build.Dockerfile b/build.Dockerfile index e7ef5433..845c8b20 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -17,14 +17,12 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ libvncserver-dev \ python3-pip \ qemu-user-static \ - tesseract-ocr \ - libtesseract-dev \ wget && \ apt-get clean && \ rm -rf /var/lib/apt/lists/ # There are issues with PYTHONHOME if using distro packages, use pip instead. -RUN pip3 install construct flake8 flask flask_restful jsonschema mnemonic pycrypto pyelftools pbkdf2 pytest Pillow requests pytesseract +RUN pip3 install construct flake8 flask flask_restful jsonschema mnemonic pycrypto pyelftools pbkdf2 pytest Pillow requests # Create SHA256SUMS, download dependencies and verify their integrity RUN \ diff --git a/docs/installation/build.md b/docs/installation/build.md index 03422617..45fdd088 100644 --- a/docs/installation/build.md +++ b/docs/installation/build.md @@ -13,9 +13,8 @@ sudo apt install \ cmake gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gdb-multiarch \ python3-pyqt5 python3-construct python3-flask-restful python3-jsonschema \ python3-mnemonic python3-pil python3-pyelftools python3-requests \ - qemu-user-static tesseract-ocr libtesseract-dev + qemu-user-static -pip install pytesseract ``` For optional VNC support, please also install `libvncserver-dev`: diff --git a/docs/user/usage.md b/docs/user/usage.md index 6cb81dce..b6c178d7 100644 --- a/docs/user/usage.md +++ b/docs/user/usage.md @@ -93,9 +93,4 @@ Launch the Bitcoin Testnet app, which requires the Bitcoin app: ## OCR -OCR is available for NanoX with built in character recognition. - -Stax makes use of the Tessseract library for OCR, with known issues for detecting inverted text. - -Launching Speculos with `--force-full-ocr` flag forces all text to be written in black over a white background, -having an visible effect on the display but solving the latter issue. +OCR is available for NanoX, Nanos S+ and Stax with built in character recognition. diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 976ba029..00000000 --- a/mypy.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..091d7017 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = [ + "setuptools>=61.2", + "wheel", + "setuptools_scm", + "cmake" +] +build-backend = "setuptools.build_meta" + +[project] +name = "speculos" +authors = [{name = "Ledger", email = "hello@ledger.fr"}] +description = "Ledger Blue, Stax and Nano S/S+/X application emulator" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", +] +requires-python = ">=3.8" +dependencies = [ + "construct>=2.10.56,<3.0.0", + "flask>=2.0.0,<3.0.0", + "flask-restful>=0.3.9,<1.0", + "jsonschema>=3.2.0,<4.18.0", + "mnemonic>=0.19,<1.0", + "pillow>=8.0.0,<11.0.0", + "pyelftools>=0.27,<1.0", + "pyqt5>=5.15.2,<6.0.0", + "requests>=2.25.1,<3.0.0", +] +dynamic = ["version"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Homepage = "https://github.com/LedgerHQ/speculos" +"Bug Tracker" = "https://github.com/LedgerHQ/speculos/issues" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov" +] + +[project.scripts] +speculos = "speculos.main:main" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages] +find = {namespaces = false} + +[tool.setuptools_scm] +write_to = "speculos/__version__.py" +local_scheme = "no-local-version" + +[tool.mypy] +ignore_missing_imports = true + +[tool.flake8] +max-line-length = 120 diff --git a/sdk/bolos_syscalls_unified_sdk.h b/sdk/bolos_syscalls_unified_sdk.h index 63fbfbc7..75201203 100644 --- a/sdk/bolos_syscalls_unified_sdk.h +++ b/sdk/bolos_syscalls_unified_sdk.h @@ -176,7 +176,8 @@ #define SYSCALL_nbgl_get_font_ID_IN 0x01fa000c #define SYSCALL_nbgl_screen_reinit_ID_IN 0x00fa000d #define SYSCALL_nbgl_front_draw_img_rle_legacy_ID_IN 0x00fa000e // API levels 7-8-9 -#define SYSCALL_nbgl_front_draw_img_rle_ID_IN 0x04fa0010 +#define SYSCALL_nbgl_front_draw_img_rle_10_ID_IN 0x04fa0010 // API level 10 +#define SYSCALL_nbgl_front_draw_img_rle_ID_IN 0x05fa0010 // API level_12 #define SYSCALL_ox_bls12381_sign_ID_IN 0x05000103 #define SYSCALL_cx_hash_to_field_ID_IN 0x06000104 diff --git a/setup.py b/setup.py index 36a20f34..08080d99 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 """Install Speculos""" import pathlib +import tempfile from distutils.spawn import find_executable from setuptools.command.build_py import build_py as _build_py -from setuptools import find_packages, setup -import sys -import tempfile +from setuptools import setup class BuildSpeculos(_build_py): @@ -42,45 +41,7 @@ def run(self): setup( - name="speculos", - author="Ledger", - author_email="hello@ledger.fr", - url="https://github.com/LedgerHQ/speculos", - python_requires=">=3.6.0", - description="Ledger Blue, Stax and Nano S/S+/X application emulator", - long_description=pathlib.Path("README.md").read_text(), - long_description_content_type="text/markdown", - packages=find_packages(), - install_requires=[ - "construct>=2.10.56,<3.0.0", - "flask>=2.0.0,<3.0.0", - "flask-restful>=0.3.9,<1.0", - "jsonschema>=3.2.0,<4.18.0", - "mnemonic>=0.19,<1.0", - "pillow>=8.0.0,<10.0.0", - "pyelftools>=0.27,<1.0", - "pyqt5>=5.15.2,<6.0.0", - "requests>=2.25.1,<3.0.0", - "pytesseract>=0.3.10,<0.4.0", - ] - + (["dataclasses>=0.8,<0.9"] if sys.version_info <= (3, 6) else []), - extras_require={ - 'dev': [ - 'pytest', - 'pytest-cov' - ]}, - use_scm_version={ - "write_to": "speculos/__version__.py", - "local_scheme": "no-local-version" - }, - setup_requires=["wheel", "setuptools_scm"], - entry_points={ - "console_scripts": [ - "speculos = speculos.main:main", - ], - }, cmdclass={ "build_py": BuildSpeculos, }, - include_package_data=True, ) diff --git a/speculos.py b/speculos.py index c258bf64..8fc3d0df 100755 --- a/speculos.py +++ b/speculos.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 """Launch Speculos emulator""" -import sys from speculos.main import main if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/speculos/api/apdu.py b/speculos/api/apdu.py index 223e0059..0424cd5d 100644 --- a/speculos/api/apdu.py +++ b/speculos/api/apdu.py @@ -18,20 +18,26 @@ def __init__(self, seph: SeProxyHal): self._seph.apdu_callbacks.append(self.seph_apdu_callback) self.response: Optional[bytes] - def exchange(self, data: bytes) -> Generator[bytes, None, None]: + def exchange(self, data: bytes, tick_timeout: int = 5 * 60 * 10) -> Generator[bytes, None, None]: # force headers to be sent yield b"" + tick_count_before_exchange = self._seph.get_tick_count() + with self.endpoint_lock: # Lock for a command/response for one client with self.response_condition: self.response = None self._seph.to_app(data) with self.response_condition: while self.response is None: - self.response_condition.wait() + self.response_condition.wait(0.1) + exchange_tick_count = self._seph.get_tick_count() - tick_count_before_exchange + + if tick_timeout != 0 and exchange_tick_count > tick_timeout: + raise TimeoutError() yield json.dumps({"data": self.response.hex()}).encode() - def seph_apdu_callback(self, data: bytes): + def seph_apdu_callback(self, data: bytes) -> None: """ Called by seph when data is transmitted by the SE. That data should be the response to a prior APDU request @@ -56,4 +62,10 @@ def post(self): return {"error": f"{e}"}, 400 data = bytes.fromhex(args.get("data")) + + if "tick_timeout" in args: + tick_timeout = args["tick_timeout"] + return Response(stream_with_context(self._bridge.exchange(data, tick_timeout)), + content_type="application/json") + return Response(stream_with_context(self._bridge.exchange(data)), content_type="application/json") diff --git a/speculos/api/api.py b/speculos/api/api.py index cf870836..3cb78216 100644 --- a/speculos/api/api.py +++ b/speculos/api/api.py @@ -1,112 +1,89 @@ import socket import threading import pkg_resources -from typing import Dict, Optional +from typing import Any, Dict from flask import Flask from flask_restful import Api -from ..mcu.readerror import ReadError -from ..mcu.seproxyhal import SeProxyHal +from speculos.mcu.display import DisplayNotifier, IODevice +from speculos.mcu.readerror import ReadError +from speculos.mcu.seproxyhal import SeProxyHal +from speculos.observer import BroadcastInterface from .apdu import APDU from .automation import Automation from .button import Button -from .events import Events, EventsBroadcaster +from .events import Events from .finger import Finger from .screenshot import Screenshot from .swagger import Swagger from .web_interface import WebInterface +from .ticker import Ticker -class ApiRunner: - """Run the Speculos API server, with a notification when it stops""" +class ApiRunner(IODevice): + """Run the Speculos API server in a dedicated thread, with a notification when it stops""" def __init__(self, api_port: int) -> None: - self._app: Optional[Flask] = None - # self.s is used by Screen.add_notifier. Closing self._notify_exit + self._api_wrapper: ApiWrapper + # self.sock is used by Screen.add_notifier. Closing self._notify_exit # signals it that the API is no longer running. - self.s: socket.socket + self.sock: socket.socket self._notify_exit: socket.socket - self.s, self._notify_exit = socket.socketpair() - self.api_port: int = api_port + self.sock, self._notify_exit = socket.socketpair() + self._port: int = api_port + self._api_thread: threading.Thread - def can_read(self, s: int, screen) -> None: - assert s == self.s.fileno() + @property + def file(self): + return self.sock + + def can_read(self, screen: DisplayNotifier) -> None: # Being able to read from the socket only happens when the API server exited. raise ReadError("API server exited") - def _run(self) -> None: - try: - # threaded must be set to allow serving requests along events streaming - self._app.run(host="0.0.0.0", port=self.api_port, threaded=True, use_reloader=False) - except Exception as exc: - self._app.logger.error("An exception occurred in the Flask API server: %s", exc) - raise exc - finally: - self._notify_exit.close() - def start_server_thread(self, - screen_, + screen_: DisplayNotifier, seph_: SeProxyHal, - automation_server: EventsBroadcaster) -> None: - wrapper = ApiWrapper(screen_, seph_, automation_server) - self._app = wrapper.app - api_thread = threading.Thread(target=self._run, name="API-server", daemon=True) - api_thread.start() + automation_server: BroadcastInterface) -> None: + self._api_wrapper = ApiWrapper(self._port, screen_, seph_, automation_server) + self._api_thread = threading.Thread(target=self._api_wrapper.run, name="API-server", daemon=True) + self._api_thread.start() + def stop(self): + self._notify_exit.close() -class ApiWrapper: - def __init__(self, screen, seph: SeProxyHal, automation_server: EventsBroadcaster): - self._screen = screen - self._seph = seph - self._set_app() - - self._api = Api(self.app) - self._api.add_resource(APDU, "/apdu", - resource_class_kwargs=self._seph_kwargs) - self._api.add_resource(Automation, "/automation", - resource_class_kwargs=self._seph_kwargs) - self._api.add_resource(Button, "/button/left", "/button/right", "/button/both", - resource_class_kwargs=self._seph_kwargs) - self._api.add_resource(Events, "/events", - resource_class_kwargs={**self._app_kwargs, - "automation_server": automation_server}) - self._api.add_resource(Finger, "/finger", - resource_class_kwargs=self._seph_kwargs) - self._api.add_resource(Screenshot, "/screenshot", - resource_class_kwargs=self._screen_kwargs) - self._api.add_resource(Swagger, "/swagger/", - resource_class_kwargs=self._app_kwargs) - self._api.add_resource(WebInterface, "/", - resource_class_kwargs=self._app_kwargs) - def _set_app(self): +class ApiWrapper: + def __init__(self, + api_port: int, + screen: DisplayNotifier, + seph: SeProxyHal, + automation_server: BroadcastInterface): + self._port = api_port static_folder = pkg_resources.resource_filename(__name__, "/static") self._app = Flask(__name__, static_url_path="", static_folder=static_folder) self._app.env = "development" - @property - def _screen_kwargs(self): - return {"screen": self.screen} - - @property - def _app_kwargs(self) -> Dict[str, Flask]: - return {"app": self.app} - - @property - def _seph_kwargs(self) -> Dict[str, SeProxyHal]: - return {"seph": self.seph} - - @property - def app(self) -> Flask: - return self._app - - @property - def api(self) -> Api: - return self._api - - @property - def screen(self): - return self._screen - - @property - def seph(self) -> SeProxyHal: - return self._seph + screen_kwargs = {"screen": screen} + seph_kwargs = {"seph": seph} + app_kwargs = {"app": self._app} + event_kwargs: Dict[str, Any] = {**app_kwargs, "automation_server": automation_server} + + self._api = Api(self._app) + + self._api.add_resource(APDU, "/apdu", resource_class_kwargs=seph_kwargs) + self._api.add_resource(Automation, "/automation", resource_class_kwargs=seph_kwargs) + self._api.add_resource(Button, + "/button/left", + "/button/right", + "/button/both", + resource_class_kwargs=seph_kwargs) + self._api.add_resource(Events, "/events", resource_class_kwargs=event_kwargs) + self._api.add_resource(Finger, "/finger", resource_class_kwargs=seph_kwargs) + self._api.add_resource(Screenshot, "/screenshot", resource_class_kwargs=screen_kwargs) + self._api.add_resource(Swagger, "/swagger/", resource_class_kwargs=app_kwargs) + self._api.add_resource(WebInterface, "/", resource_class_kwargs=app_kwargs) + self._api.add_resource(Ticker, "/ticker/", resource_class_kwargs=seph_kwargs) + + def run(self): + # threaded must be set to allow serving requests along events streaming + self._app.run(host="0.0.0.0", port=self._port, threaded=True, use_reloader=False) diff --git a/speculos/api/events.py b/speculos/api/events.py index d0e8372c..71dc7230 100644 --- a/speculos/api/events.py +++ b/speculos/api/events.py @@ -1,41 +1,33 @@ import json import logging import threading -from typing import Optional, List +from typing import Dict, Generator, List, Optional, Tuple, Union from dataclasses import asdict from flask import stream_with_context, Response from flask_restful import inputs, reqparse +from speculos.observer import BroadcastInterface, ObserverInterface, TextEvent from .restful import AppResource -from ..mcu.automation import TextEvent # Approximative minimum vertical distance between two lines of text on the devices' screen. MIN_LINES_HEIGHT_DISTANCE = 10 # pixels -class EventsBroadcaster: +class EventsBroadcaster(BroadcastInterface): """This used to be the 'Automation Server'.""" - def __init__(self): - self.clients = [] + def __init__(self) -> None: + super().__init__() self.screen_content: List[TextEvent] = [] self.events: List[TextEvent] = [] self.condition = threading.Condition() self.logger = logging.getLogger("events") - def add_client(self, client): - self.logger.debug("events: new client") - self.clients.append(client) - - def remove_client(self, client): - self.logger.debug("events: client exited") - self.clients.remove(client) - - def clear_events(self): + def clear_events(self) -> None: self.logger.debug("Clearing events") self.screen_content = [] - def broadcast(self, event: TextEvent): + def broadcast(self, event: TextEvent) -> None: if self.screen_content: # Reset screen content if new event is not below the last text line of # current screen. Event Y coordinate starts at the top of the screen. @@ -51,12 +43,12 @@ def broadcast(self, event: TextEvent): self.condition.notify_all() -class EventClient: - def __init__(self, broadcaster: EventsBroadcaster): +class EventClient(ObserverInterface): + def __init__(self, broadcaster: EventsBroadcaster) -> None: self.events: List[TextEvent] = [] self._broadcaster = broadcaster - def generate(self): + def generate(self) -> Generator[bytes, None, None]: try: # force headers to be sent yield b"" @@ -74,12 +66,12 @@ def generate(self): finally: self._broadcaster.remove_client(self) - def send_screen_event(self, event): + def send_screen_event(self, event: TextEvent) -> None: self.events.append(event) class Events(AppResource): - def __init__(self, *args, automation_server: Optional[EventsBroadcaster] = None, **kwargs): + def __init__(self, *args, automation_server: Optional[EventsBroadcaster] = None, **kwargs) -> None: if automation_server is None: raise RuntimeError("Argument 'automation_server' must not be None") self._broadcaster = automation_server @@ -88,7 +80,7 @@ def __init__(self, *args, automation_server: Optional[EventsBroadcaster] = None, self.parser.add_argument("currentscreenonly", type=inputs.boolean, default=False, location='values') super().__init__(*args, **kwargs) - def get(self): + def get(self) -> Union[Response, Tuple[Dict[str, List], int]]: args = self.parser.parse_args() if args.stream: client = EventClient(self._broadcaster) @@ -100,6 +92,6 @@ def get(self): event_list = self._broadcaster.events return {"events": [asdict(e) for e in event_list]}, 200 - def delete(self): + def delete(self) -> Tuple[Dict, int]: self._broadcaster.events.clear() return {}, 200 diff --git a/speculos/api/resources/apdu.schema b/speculos/api/resources/apdu.schema index 220fd196..c4b61f6b 100644 --- a/speculos/api/resources/apdu.schema +++ b/speculos/api/resources/apdu.schema @@ -3,7 +3,8 @@ "type": "object", "properties": { - "data": { "type": "string", "pattern": "^([a-fA-F0-9]{2})+$" } + "data": { "type": "string", "pattern": "^([a-fA-F0-9]{2})+$" }, + "tick_timeout": { "type": "number"} }, "required": [ "data" ], "additionalProperties": false diff --git a/speculos/api/resources/ticker.schema b/speculos/api/resources/ticker.schema new file mode 100644 index 00000000..68a67fc9 --- /dev/null +++ b/speculos/api/resources/ticker.schema @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "type": "object", + "properties": { + "action": { "enum": [ "pause", "resume", "single-step"] } + }, + "required": [ "action" ], + "additionalProperties": false +} diff --git a/speculos/api/screenshot.py b/speculos/api/screenshot.py index 4d9d955f..adc08e80 100644 --- a/speculos/api/screenshot.py +++ b/speculos/api/screenshot.py @@ -5,7 +5,7 @@ class Screenshot(ScreenResource): def get(self): - iobytes_value = self.screen.m.get_public_screenshot() + iobytes_value = self.screen.display.m.get_public_screenshot() response = Response(iobytes_value, mimetype="image/png") response.headers.add("Cache-control", "no-cache,no-store") return response diff --git a/speculos/api/ticker.py b/speculos/api/ticker.py new file mode 100644 index 00000000..2f227b2a --- /dev/null +++ b/speculos/api/ticker.py @@ -0,0 +1,20 @@ +import jsonschema +from flask import request + +from .helpers import load_json_schema +from .restful import SephResource + + +class Ticker(SephResource): + schema = load_json_schema("ticker.schema") + + def post(self): + args = request.get_json(force=True) + try: + jsonschema.validate(instance=args, schema=self.schema) + except jsonschema.exceptions.ValidationError as e: + return {"error": f"{e}"}, 400 + + action = args["action"] + self.seph.handle_ticker_request(action) + return {}, 200 diff --git a/speculos/client.py b/speculos/client.py index 387e71f7..5332b818 100644 --- a/speculos/client.py +++ b/speculos/client.py @@ -1,7 +1,3 @@ -from contextlib import contextmanager -from PIL import Image, ImageChops -from typing import Generator, List, Optional, Tuple, Type -from types import TracebackType import json import logging import requests @@ -9,6 +5,11 @@ import sys import subprocess import time +from contextlib import contextmanager +from PIL import Image, ImageChops +from requests import Response +from typing import Generator, List, Optional, Tuple, Type +from types import TracebackType logger = logging.getLogger("speculos-client") logger.setLevel(logging.INFO) @@ -70,7 +71,7 @@ def __init__(self, api_url: str) -> None: self.api_url = api_url self.timeout = 2000 self.session = requests.Session() - self.stream = None + self.stream: Optional[Response] = None def open_stream(self) -> None: assert self.stream is None @@ -94,6 +95,7 @@ def get_next_event(self) -> dict: https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream """ + assert self.stream is not None data = b"" while True: line = self.stream.raw.readline() @@ -129,15 +131,26 @@ def finger_touch(self, x: int, y: int, delay: float = 0.5) -> None: with self.session.post(f"{self.api_url}/finger", json=data) as response: check_status_code(response, "/finger") + def ticker_ctl(self, action: str) -> None: + data = {"action": action} + with self.session.post(f"{self.api_url}/ticker", json=data) as response: + check_status_code(response, "/ticker") + def get_screenshot(self) -> bytes: with self.session.get(f"{self.api_url}/screenshot") as response: check_status_code(response, "/screenshot") return response.content - def _apdu_exchange(self, data: bytes) -> bytes: - with self.session.post(f"{self.api_url}/apdu", json={"data": data.hex()}) as response: - apdu_response = ApduResponse(response) - return apdu_response.receive() + def _apdu_exchange(self, data: bytes, tick_timeout: int = 5 * 60 * 10) -> bytes: + try: + data_payload = {"data": data.hex(), "tick_timeout": tick_timeout} + with self.session.post(f"{self.api_url}/apdu", json=data_payload) as response: + apdu_response = ApduResponse(response) + return apdu_response.receive() + + # TimeoutError exception in exchange function raises ChunkedEncodingError exception + except requests.exceptions.ChunkedEncodingError: + raise TimeoutError() def _apdu_exchange_nowait(self, data: bytes) -> requests.Response: return self.session.post(f"{self.api_url}/apdu", json={"data": data.hex()}, stream=True) @@ -232,9 +245,11 @@ def __exit__( ) -> None: self.stop() - def apdu_exchange(self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0) -> bytes: + def apdu_exchange( + self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0, + tick_timeout: int = 5 * 60 * 10) -> bytes: apdu = bytes([cla, ins, p1, p2, len(data)]) + data - return Api._apdu_exchange(self, apdu) + return Api._apdu_exchange(self, apdu, tick_timeout) @contextmanager def apdu_exchange_nowait( diff --git a/speculos/cxlib/nanosp-api-level-cx-12.elf b/speculos/cxlib/nanosp-api-level-cx-12.elf new file mode 100755 index 00000000..68db17f1 Binary files /dev/null and b/speculos/cxlib/nanosp-api-level-cx-12.elf differ diff --git a/speculos/cxlib/nanox-api-level-cx-12.elf b/speculos/cxlib/nanox-api-level-cx-12.elf new file mode 100755 index 00000000..fde4000d Binary files /dev/null and b/speculos/cxlib/nanox-api-level-cx-12.elf differ diff --git a/speculos/cxlib/stax-api-level-cx-12.elf b/speculos/cxlib/stax-api-level-cx-12.elf new file mode 100755 index 00000000..481a9e04 Binary files /dev/null and b/speculos/cxlib/stax-api-level-cx-12.elf differ diff --git a/speculos/cxlib/stax-api-level-cx-13.elf b/speculos/cxlib/stax-api-level-cx-13.elf new file mode 100755 index 00000000..481a9e04 Binary files /dev/null and b/speculos/cxlib/stax-api-level-cx-13.elf differ diff --git a/speculos/cxlib/stax-api-level-cx-14.elf b/speculos/cxlib/stax-api-level-cx-14.elf new file mode 100644 index 00000000..ffad0c76 Binary files /dev/null and b/speculos/cxlib/stax-api-level-cx-14.elf differ diff --git a/speculos/cxlib/stax-api-level-cx-15.elf b/speculos/cxlib/stax-api-level-cx-15.elf new file mode 100755 index 00000000..7a312c04 Binary files /dev/null and b/speculos/cxlib/stax-api-level-cx-15.elf differ diff --git a/speculos/fonts/bagl_font_open_sans_extrabold_11px-api-level-5.json b/speculos/fonts/bagl_font_open_sans_extrabold_11px-api-level-5.json deleted file mode 100644 index ee66dca2..00000000 --- a/speculos/fonts/bagl_font_open_sans_extrabold_11px-api-level-5.json +++ /dev/null @@ -1,784 +0,0 @@ -[ - { - "bitmap": "//M7JSSSn/KXSCSItywOh6YfAk5Ywwr24zesYQ05PGzY4ODtnj3fF2Q2M2NG2Wx7C0z7ex4M8zMM1v8PGDFmjBie2Ww2m808zD3GGGPOhmGMY3wPhjkYhj0YDkej+WEwb4zHGHscg+Azm808H4ZhDMMYntnMM5vNPJ7ZbOaDYRwPDzZgDZiZYRgGP/ADwzDMzA+GOQZgGPgwZvfWS6+9nwH4ARg8PH5mfufD37Y927Z93jAMwzB4n9lsNpvNPm+MN8Z4f4w3xhi+wWA3m818s9nsN5vNZtu2bYwxxhhjjB3z2ebx2czmY4wxxvjHHffee++ttttuZ2dvb3t7c3M+Y2NjY2NjPt+2bd8wDD5jY2NjY2M+MGCPzWbz2GxmfozjOH8/wzAMwzCz2Ww2m8088zzPnucxc7ab3axmNe5wBxvDZjw8PDxmw2ObjYPBYDB/GA7DcBj+PzMzM/NDGMMYwo8xxhhj7AEMhkcyAz/DHphvNvMBw/Bt+759vHEcBg8wGM92u9l4PPP/YOABnPEZhmEYfpvNY/CNxz7D8G3btm3Dtm0MMMYYY4wdw7B9z7Pt27Zt7bZt27ZtG922bdsGHNv9buPftu/bNwwDPtvtZvPBYDD9MzNvPI8f5htjDAfbtm3bBWObzcbhM9vmn3/MMAN3G4djuwNjm43G4TAYBj/GMMYPnDF2DmOMAzMzMzMzA8cYw7kx5m8PL6WUUno=", - "bagl_font_character": [ - { - "char": 32, - "char_width": 4, - "x_offset": 0, - "y_offset": 6, - "bitmap_byte_count": 0, - "bitmap_offset": 0 - }, - { - "char": 33, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 0 - }, - { - "char": 34, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 2 - }, - { - "char": 35, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 4 - }, - { - "char": 36, - "char_width": 7, - "x_offset": 1, - "y_offset": 0, - "bitmap_byte_count": 8, - "bitmap_offset": 11 - }, - { - "char": 37, - "char_width": 12, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 19 - }, - { - "char": 38, - "char_width": 10, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 30 - }, - { - "char": 39, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 1, - "bitmap_offset": 39 - }, - { - "char": 40, - "char_width": 5, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 40 - }, - { - "char": 41, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 45 - }, - { - "char": 42, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 49 - }, - { - "char": 43, - "char_width": 7, - "x_offset": 1, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 53 - }, - { - "char": 44, - "char_width": 4, - "x_offset": 1, - "y_offset": 7, - "bitmap_byte_count": 1, - "bitmap_offset": 57 - }, - { - "char": 45, - "char_width": 5, - "x_offset": 1, - "y_offset": 5, - "bitmap_byte_count": 1, - "bitmap_offset": 58 - }, - { - "char": 46, - "char_width": 4, - "x_offset": 2, - "y_offset": 7, - "bitmap_byte_count": 1, - "bitmap_offset": 59 - }, - { - "char": 47, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 60 - }, - { - "char": 48, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 65 - }, - { - "char": 49, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 72 - }, - { - "char": 50, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 77 - }, - { - "char": 51, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 83 - }, - { - "char": 52, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 89 - }, - { - "char": 53, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 96 - }, - { - "char": 54, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 101 - }, - { - "char": 55, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 108 - }, - { - "char": 56, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 114 - }, - { - "char": 57, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 121 - }, - { - "char": 58, - "char_width": 4, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 2, - "bitmap_offset": 128 - }, - { - "char": 59, - "char_width": 4, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 3, - "bitmap_offset": 130 - }, - { - "char": 60, - "char_width": 6, - "x_offset": 1, - "y_offset": 2, - "bitmap_byte_count": 5, - "bitmap_offset": 133 - }, - { - "char": 61, - "char_width": 7, - "x_offset": 1, - "y_offset": 4, - "bitmap_byte_count": 3, - "bitmap_offset": 138 - }, - { - "char": 62, - "char_width": 6, - "x_offset": 1, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 141 - }, - { - "char": 63, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 145 - }, - { - "char": 64, - "char_width": 11, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 151 - }, - { - "char": 65, - "char_width": 9, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 163 - }, - { - "char": 66, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 171 - }, - { - "char": 67, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 177 - }, - { - "char": 68, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 183 - }, - { - "char": 69, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 190 - }, - { - "char": 70, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 195 - }, - { - "char": 71, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 200 - }, - { - "char": 72, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 207 - }, - { - "char": 73, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 214 - }, - { - "char": 74, - "char_width": 5, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 217 - }, - { - "char": 75, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 224 - }, - { - "char": 76, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 231 - }, - { - "char": 77, - "char_width": 12, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 236 - }, - { - "char": 78, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 246 - }, - { - "char": 79, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 254 - }, - { - "char": 80, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 262 - }, - { - "char": 81, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 268 - }, - { - "char": 82, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 278 - }, - { - "char": 83, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 285 - }, - { - "char": 84, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 290 - }, - { - "char": 85, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 296 - }, - { - "char": 86, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 303 - }, - { - "char": 87, - "char_width": 12, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 309 - }, - { - "char": 88, - "char_width": 9, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 320 - }, - { - "char": 89, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 328 - }, - { - "char": 90, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 335 - }, - { - "char": 91, - "char_width": 6, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 342 - }, - { - "char": 92, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 347 - }, - { - "char": 93, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 352 - }, - { - "char": 94, - "char_width": 7, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 359 - }, - { - "char": 95, - "char_width": 7, - "x_offset": 0, - "y_offset": 10, - "bitmap_byte_count": 1, - "bitmap_offset": 364 - }, - { - "char": 96, - "char_width": 8, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 1, - "bitmap_offset": 365 - }, - { - "char": 97, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 6, - "bitmap_offset": 366 - }, - { - "char": 98, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 372 - }, - { - "char": 99, - "char_width": 7, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 378 - }, - { - "char": 100, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 383 - }, - { - "char": 101, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 6, - "bitmap_offset": 390 - }, - { - "char": 102, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 396 - }, - { - "char": 103, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 8, - "bitmap_offset": 402 - }, - { - "char": 104, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 410 - }, - { - "char": 105, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 416 - }, - { - "char": 106, - "char_width": 5, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 419 - }, - { - "char": 107, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 426 - }, - { - "char": 108, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 432 - }, - { - "char": 109, - "char_width": 11, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 435 - }, - { - "char": 110, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 442 - }, - { - "char": 111, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 447 - }, - { - "char": 112, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 452 - }, - { - "char": 113, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 8, - "bitmap_offset": 459 - }, - { - "char": 114, - "char_width": 6, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 3, - "bitmap_offset": 467 - }, - { - "char": 115, - "char_width": 7, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 4, - "bitmap_offset": 470 - }, - { - "char": 116, - "char_width": 6, - "x_offset": 1, - "y_offset": 2, - "bitmap_byte_count": 5, - "bitmap_offset": 474 - }, - { - "char": 117, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 479 - }, - { - "char": 118, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 484 - }, - { - "char": 119, - "char_width": 11, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 8, - "bitmap_offset": 489 - }, - { - "char": 120, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 6, - "bitmap_offset": 497 - }, - { - "char": 121, - "char_width": 8, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 8, - "bitmap_offset": 503 - }, - { - "char": 122, - "char_width": 7, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 511 - }, - { - "char": 123, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 516 - }, - { - "char": 124, - "char_width": 7, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 523 - }, - { - "char": 125, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 529 - }, - { - "char": 126, - "char_width": 7, - "x_offset": 1, - "y_offset": 4, - "bitmap_byte_count": 2, - "bitmap_offset": 535 - }, - { - "char": 127, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 537 - } - ], - "bagl_font": { - "font_id": 8, - "bpp": 1, - "char_height": 12, - "baseline_height": 9, - "char_leftmost_x": -1, - "first_char": 32, - "last_char": 127 - } - } -] diff --git a/speculos/fonts/bagl_font_open_sans_light_16px-api-level-5.json b/speculos/fonts/bagl_font_open_sans_light_16px-api-level-5.json deleted file mode 100644 index 95362067..00000000 --- a/speculos/fonts/bagl_font_open_sans_light_16px-api-level-5.json +++ /dev/null @@ -1,784 +0,0 @@ -[ - { - "bitmap": "VVVQKaUEkECCCCL+I4HEf0QQQQIJCDwKCQkKHDhISCkeCAaRCImQBEmQMqYESZCESIhEMByIIIIIFDAgUUhBBTKMT1VSkiRJEgmJJEmSpAQICAh/FBQiCAgIfwgICJICBxsQgiAEQRCCIAQcIkFBQUFBQUFBIhwEQ4FAIBAIBAKBHiEgICAgEAgEAgF/HiFAIDAeIEBAQCAfQMBAQYKEiBAh/4EAAQI+AgICAR9gQEBAIR44BgIBAT1DQUFBQjx/QCAgIBAQCAgIBAQcIkFBIhwiQUFBQT4eIUFBQWFeQEAgMA4bAGADGwAgCUAwDAMMMEB/AAB/AQYYYBgGAQ8EQRBCCAIgCPCBQQgQ8RSRElJCSkiJKe4EAAFAAPABMMAAAxJIIEEIPwIJJFCAHyFBQSEfIUFBQSEf+AgICBAgQIAAAQQQwAcfQgQJFChQoECBgoT4v0AgEPgFAoFA4Ad/EARB8AdBEAT4IYAAAhCAAIQvQAESEIHwA4EEEkgggfwTSCCBBBJIIFVVVQRBEARBEARBEAQxwVAkURgUEpFIKAhBEARBEARBEPwBEjADMzCFUiiFkiRJEiMxEiCBDDJIIYUkEkkkoQQTTCB4IIRAAhSgAAUoQAESCCHwn1AoFAp9AoFAIHgghEACFKAABShAARIIIfAABEAABh8hQUFBIR8RESFBQT5BAQEBDjBAQEBAP38EAoFAIBAIBAIBgQQSSCCBBBJIIIEEIgQPAQUkiCCCEEEEESigAAEEwaAwJAyJRCIRSUKSUCgUCoaBYSAQggQRQYICAgoURIgICgiBhAghQQIDBggQIECAf0AgIBAICAQCAgF/HxEREREREQ+BIAgEQRAIgkAPIYQQQgghhNADCBQUIiJBQX+BQB4QCOQLhWIuAQIECNBjSKBAgQIFGtIDPEEgEAgEBDyAAAECxEtYoECBAgUSxgscIkFBfwEBQjw4QRAfQRAEQRAE/CJCQiI8BAJ8QoFDPgEBAQEdI0FBQUFBQUEBkiRJBABCCCGEEELIAYFAIBCKJAqFRSQUBEmSJEkSnWOMBKEgFISCUBAKQkEIHSNBQUFBQUFBHCJBQUFBQSIcPYYEChQoUKAhPQIECBC8hAUKFChQIGG8AAECBAh9hBBCCAFeEAwMBEEP4gkhhBCCA0FBQUFBQUFiXkFBIiIiFBQUCGEoRmIkSZJEKQyDEAgBoZCEwWBIQiFBQSIiIhQUFAgIBAQDH4QghCAEHxhBEARBDARBEASBASGEEEIIIYQQQggBQxBCCMGEEELEB3g/ISEhISEhISEhPw==", - "bagl_font_character": [ - { - "char": 32, - "char_width": 6, - "x_offset": 0, - "y_offset": 8, - "bitmap_byte_count": 0, - "bitmap_offset": 0 - }, - { - "char": 33, - "char_width": 5, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 0 - }, - { - "char": 34, - "char_width": 8, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 3 - }, - { - "char": 35, - "char_width": 12, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 6 - }, - { - "char": 36, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 13, - "bitmap_offset": 21 - }, - { - "char": 37, - "char_width": 15, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 18, - "bitmap_offset": 34 - }, - { - "char": 38, - "char_width": 13, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 52 - }, - { - "char": 39, - "char_width": 5, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 1, - "bitmap_offset": 67 - }, - { - "char": 40, - "char_width": 6, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 68 - }, - { - "char": 41, - "char_width": 6, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 74 - }, - { - "char": 42, - "char_width": 11, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 7, - "bitmap_offset": 80 - }, - { - "char": 43, - "char_width": 11, - "x_offset": 3, - "y_offset": 2, - "bitmap_byte_count": 7, - "bitmap_offset": 87 - }, - { - "char": 44, - "char_width": 5, - "x_offset": 2, - "y_offset": 11, - "bitmap_byte_count": 2, - "bitmap_offset": 94 - }, - { - "char": 45, - "char_width": 7, - "x_offset": 3, - "y_offset": 8, - "bitmap_byte_count": 1, - "bitmap_offset": 96 - }, - { - "char": 46, - "char_width": 6, - "x_offset": 3, - "y_offset": 11, - "bitmap_byte_count": 1, - "bitmap_offset": 97 - }, - { - "char": 47, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 98 - }, - { - "char": 48, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 107 - }, - { - "char": 49, - "char_width": 11, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 119 - }, - { - "char": 50, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 129 - }, - { - "char": 51, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 141 - }, - { - "char": 52, - "char_width": 11, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 14, - "bitmap_offset": 153 - }, - { - "char": 53, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 167 - }, - { - "char": 54, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 179 - }, - { - "char": 55, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 191 - }, - { - "char": 56, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 203 - }, - { - "char": 57, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 215 - }, - { - "char": 58, - "char_width": 6, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 4, - "bitmap_offset": 227 - }, - { - "char": 59, - "char_width": 6, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 4, - "bitmap_offset": 231 - }, - { - "char": 60, - "char_width": 11, - "x_offset": 3, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 235 - }, - { - "char": 61, - "char_width": 11, - "x_offset": 3, - "y_offset": 5, - "bitmap_byte_count": 4, - "bitmap_offset": 242 - }, - { - "char": 62, - "char_width": 11, - "x_offset": 3, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 246 - }, - { - "char": 63, - "char_width": 9, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 253 - }, - { - "char": 64, - "char_width": 13, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 23, - "bitmap_offset": 262 - }, - { - "char": 65, - "char_width": 12, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 285 - }, - { - "char": 66, - "char_width": 12, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 300 - }, - { - "char": 67, - "char_width": 12, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 14, - "bitmap_offset": 312 - }, - { - "char": 68, - "char_width": 13, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 13, - "bitmap_offset": 326 - }, - { - "char": 69, - "char_width": 11, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 339 - }, - { - "char": 70, - "char_width": 10, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 350 - }, - { - "char": 71, - "char_width": 14, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 17, - "bitmap_offset": 359 - }, - { - "char": 72, - "char_width": 14, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 376 - }, - { - "char": 73, - "char_width": 6, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 391 - }, - { - "char": 74, - "char_width": 6, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 394 - }, - { - "char": 75, - "char_width": 11, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 405 - }, - { - "char": 76, - "char_width": 10, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 416 - }, - { - "char": 77, - "char_width": 14, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 18, - "bitmap_offset": 425 - }, - { - "char": 78, - "char_width": 14, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 443 - }, - { - "char": 79, - "char_width": 14, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 16, - "bitmap_offset": 458 - }, - { - "char": 80, - "char_width": 11, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 474 - }, - { - "char": 81, - "char_width": 14, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 21, - "bitmap_offset": 484 - }, - { - "char": 82, - "char_width": 12, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 505 - }, - { - "char": 83, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 517 - }, - { - "char": 84, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 11, - "bitmap_offset": 529 - }, - { - "char": 85, - "char_width": 14, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 540 - }, - { - "char": 86, - "char_width": 12, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 15, - "bitmap_offset": 555 - }, - { - "char": 87, - "char_width": 15, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 21, - "bitmap_offset": 570 - }, - { - "char": 88, - "char_width": 11, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 14, - "bitmap_offset": 591 - }, - { - "char": 89, - "char_width": 11, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 13, - "bitmap_offset": 605 - }, - { - "char": 90, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 618 - }, - { - "char": 91, - "char_width": 7, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 630 - }, - { - "char": 92, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 638 - }, - { - "char": 93, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 647 - }, - { - "char": 94, - "char_width": 11, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 657 - }, - { - "char": 95, - "char_width": 9, - "x_offset": 1, - "y_offset": 14, - "bitmap_byte_count": 1, - "bitmap_offset": 664 - }, - { - "char": 96, - "char_width": 11, - "x_offset": 5, - "y_offset": 0, - "bitmap_byte_count": 2, - "bitmap_offset": 665 - }, - { - "char": 97, - "char_width": 10, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 8, - "bitmap_offset": 667 - }, - { - "char": 98, - "char_width": 12, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 15, - "bitmap_offset": 675 - }, - { - "char": 99, - "char_width": 10, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 8, - "bitmap_offset": 690 - }, - { - "char": 100, - "char_width": 12, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 15, - "bitmap_offset": 698 - }, - { - "char": 101, - "char_width": 11, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 9, - "bitmap_offset": 713 - }, - { - "char": 102, - "char_width": 8, - "x_offset": 2, - "y_offset": 0, - "bitmap_byte_count": 10, - "bitmap_offset": 722 - }, - { - "char": 103, - "char_width": 10, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 13, - "bitmap_offset": 732 - }, - { - "char": 104, - "char_width": 11, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 13, - "bitmap_offset": 745 - }, - { - "char": 105, - "char_width": 6, - "x_offset": 3, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 758 - }, - { - "char": 106, - "char_width": 7, - "x_offset": 2, - "y_offset": 2, - "bitmap_byte_count": 10, - "bitmap_offset": 762 - }, - { - "char": 107, - "char_width": 10, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 12, - "bitmap_offset": 772 - }, - { - "char": 108, - "char_width": 6, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 5, - "bitmap_offset": 784 - }, - { - "char": 109, - "char_width": 14, - "x_offset": 1, - "y_offset": 4, - "bitmap_byte_count": 15, - "bitmap_offset": 789 - }, - { - "char": 110, - "char_width": 11, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 9, - "bitmap_offset": 804 - }, - { - "char": 111, - "char_width": 11, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 9, - "bitmap_offset": 813 - }, - { - "char": 112, - "char_width": 12, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 14, - "bitmap_offset": 822 - }, - { - "char": 113, - "char_width": 12, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 15, - "bitmap_offset": 836 - }, - { - "char": 114, - "char_width": 8, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 6, - "bitmap_offset": 851 - }, - { - "char": 115, - "char_width": 9, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 7, - "bitmap_offset": 857 - }, - { - "char": 116, - "char_width": 7, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 864 - }, - { - "char": 117, - "char_width": 11, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 9, - "bitmap_offset": 871 - }, - { - "char": 118, - "char_width": 10, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 9, - "bitmap_offset": 880 - }, - { - "char": 119, - "char_width": 14, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 14, - "bitmap_offset": 889 - }, - { - "char": 120, - "char_width": 10, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 8, - "bitmap_offset": 903 - }, - { - "char": 121, - "char_width": 10, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 13, - "bitmap_offset": 911 - }, - { - "char": 122, - "char_width": 9, - "x_offset": 3, - "y_offset": 4, - "bitmap_byte_count": 7, - "bitmap_offset": 924 - }, - { - "char": 123, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 12, - "bitmap_offset": 931 - }, - { - "char": 124, - "char_width": 8, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 11, - "bitmap_offset": 943 - }, - { - "char": 125, - "char_width": 8, - "x_offset": 3, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 954 - }, - { - "char": 126, - "char_width": 11, - "x_offset": 3, - "y_offset": 6, - "bitmap_byte_count": 2, - "bitmap_offset": 963 - }, - { - "char": 127, - "char_width": 12, - "x_offset": 4, - "y_offset": 2, - "bitmap_byte_count": 11, - "bitmap_offset": 965 - } - ], - "bagl_font": { - "font_id": 9, - "bpp": 1, - "char_height": 17, - "baseline_height": 13, - "char_leftmost_x": -2, - "first_char": 32, - "last_char": 127 - } - } -] diff --git a/speculos/fonts/bagl_font_open_sans_regular_11px-api-level-5.json b/speculos/fonts/bagl_font_open_sans_regular_11px-api-level-5.json deleted file mode 100644 index 014304c1..00000000 --- a/speculos/fonts/bagl_font_open_sans_regular_11px-api-level-5.json +++ /dev/null @@ -1,784 +0,0 @@ -[ - { - "bitmap": "VUUbIJKfQvlJBK8Uw9gDIxUVbVtUVGIOCQkGJSkRLwOSkkQSlWoFBPEzEsiPUgMBSEQiEiallFIyphBCCCEHIUREeAchgxA6EAxFIvlBIC+EhxA6LoSXUjIPIUKEECYlk1IyJqX0EDoBBAIAKYiMQRAPPEEwJgKHSCIgfATlKlWqVL8APggKhSLyiYJfFD1RFH3eEARBMHifUCgUCoU+L4QXQng/hPBDCL5AIBALhXyhUOgXCoVCVVVERERENFFSDEUShSGEEEL4w4YNK1UqU6ZMoVGplErFQj5BQUFBQUE+TxRFTxAEPkFBQUFBQT4QIE8URU8SRS6EwRA6H0EQBEEQoVAoFAqFPEGRSEShUBAxyiSTUkopRQghIQmDwZBIQqEkMQiCIA8RIkJ4T5IkOSEiRIRHREREdAwjSSEfIQ+9lB5B8ERRFD0eEeEQ5EVRFHkuvRAcTDwhhBC+JDmCF4YeQfBEURRFUVUERERERAchpDJKSlVV7yJFihQpEk8URVEEThRFkQNPFEXREwQBXhRFkQdBEE+SF0N48iIiDlEURdEHoSRJDAMRValSRYQIKRmTEqEkSQxDEAMfESI+lpRIMkmSJEkjIkQiMoMBXxRFURR9", - "bagl_font_character": [ - { - "char": 32, - "char_width": 4, - "x_offset": 0, - "y_offset": 6, - "bitmap_byte_count": 0, - "bitmap_offset": 0 - }, - { - "char": 33, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 0 - }, - { - "char": 34, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 1, - "bitmap_offset": 2 - }, - { - "char": 35, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 3 - }, - { - "char": 36, - "char_width": 7, - "x_offset": 2, - "y_offset": 2, - "bitmap_byte_count": 5, - "bitmap_offset": 10 - }, - { - "char": 37, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 15 - }, - { - "char": 38, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 23 - }, - { - "char": 39, - "char_width": 3, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 1, - "bitmap_offset": 31 - }, - { - "char": 40, - "char_width": 4, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 32 - }, - { - "char": 41, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 3, - "bitmap_offset": 36 - }, - { - "char": 42, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 39 - }, - { - "char": 43, - "char_width": 7, - "x_offset": 1, - "y_offset": 4, - "bitmap_byte_count": 2, - "bitmap_offset": 43 - }, - { - "char": 44, - "char_width": 4, - "x_offset": 1, - "y_offset": 8, - "bitmap_byte_count": 1, - "bitmap_offset": 45 - }, - { - "char": 45, - "char_width": 5, - "x_offset": 2, - "y_offset": 6, - "bitmap_byte_count": 1, - "bitmap_offset": 46 - }, - { - "char": 46, - "char_width": 4, - "x_offset": 2, - "y_offset": 8, - "bitmap_byte_count": 1, - "bitmap_offset": 47 - }, - { - "char": 47, - "char_width": 5, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 48 - }, - { - "char": 48, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 52 - }, - { - "char": 49, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 57 - }, - { - "char": 50, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 62 - }, - { - "char": 51, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 67 - }, - { - "char": 52, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 72 - }, - { - "char": 53, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 79 - }, - { - "char": 54, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 84 - }, - { - "char": 55, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 89 - }, - { - "char": 56, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 94 - }, - { - "char": 57, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 99 - }, - { - "char": 58, - "char_width": 4, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 2, - "bitmap_offset": 104 - }, - { - "char": 59, - "char_width": 4, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 3, - "bitmap_offset": 106 - }, - { - "char": 60, - "char_width": 7, - "x_offset": 2, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 109 - }, - { - "char": 61, - "char_width": 7, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 2, - "bitmap_offset": 113 - }, - { - "char": 62, - "char_width": 7, - "x_offset": 2, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 115 - }, - { - "char": 63, - "char_width": 6, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 119 - }, - { - "char": 64, - "char_width": 11, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 123 - }, - { - "char": 65, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 133 - }, - { - "char": 66, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 140 - }, - { - "char": 67, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 146 - }, - { - "char": 68, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 152 - }, - { - "char": 69, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 159 - }, - { - "char": 70, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 164 - }, - { - "char": 71, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 169 - }, - { - "char": 72, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 176 - }, - { - "char": 73, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 183 - }, - { - "char": 74, - "char_width": 4, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 185 - }, - { - "char": 75, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 190 - }, - { - "char": 76, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 196 - }, - { - "char": 77, - "char_width": 11, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 9, - "bitmap_offset": 201 - }, - { - "char": 78, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 210 - }, - { - "char": 79, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 8, - "bitmap_offset": 217 - }, - { - "char": 80, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 225 - }, - { - "char": 81, - "char_width": 10, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 231 - }, - { - "char": 82, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 241 - }, - { - "char": 83, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 247 - }, - { - "char": 84, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 252 - }, - { - "char": 85, - "char_width": 9, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 258 - }, - { - "char": 86, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 265 - }, - { - "char": 87, - "char_width": 11, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 10, - "bitmap_offset": 272 - }, - { - "char": 88, - "char_width": 8, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 7, - "bitmap_offset": 282 - }, - { - "char": 89, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 289 - }, - { - "char": 90, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 295 - }, - { - "char": 91, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 300 - }, - { - "char": 92, - "char_width": 5, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 304 - }, - { - "char": 93, - "char_width": 5, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 308 - }, - { - "char": 94, - "char_width": 7, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 313 - }, - { - "char": 95, - "char_width": 6, - "x_offset": 0, - "y_offset": 10, - "bitmap_byte_count": 1, - "bitmap_offset": 317 - }, - { - "char": 96, - "char_width": 7, - "x_offset": 3, - "y_offset": 0, - "bitmap_byte_count": 1, - "bitmap_offset": 318 - }, - { - "char": 97, - "char_width": 7, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 4, - "bitmap_offset": 319 - }, - { - "char": 98, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 323 - }, - { - "char": 99, - "char_width": 6, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 3, - "bitmap_offset": 329 - }, - { - "char": 100, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 332 - }, - { - "char": 101, - "char_width": 7, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 4, - "bitmap_offset": 338 - }, - { - "char": 102, - "char_width": 6, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 342 - }, - { - "char": 103, - "char_width": 7, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 347 - }, - { - "char": 104, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 354 - }, - { - "char": 105, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 360 - }, - { - "char": 106, - "char_width": 4, - "x_offset": 0, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 362 - }, - { - "char": 107, - "char_width": 7, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 368 - }, - { - "char": 108, - "char_width": 4, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 2, - "bitmap_offset": 373 - }, - { - "char": 109, - "char_width": 11, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 375 - }, - { - "char": 110, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 382 - }, - { - "char": 111, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 387 - }, - { - "char": 112, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 392 - }, - { - "char": 113, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 399 - }, - { - "char": 114, - "char_width": 5, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 2, - "bitmap_offset": 406 - }, - { - "char": 115, - "char_width": 6, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 3, - "bitmap_offset": 408 - }, - { - "char": 116, - "char_width": 5, - "x_offset": 1, - "y_offset": 2, - "bitmap_byte_count": 4, - "bitmap_offset": 411 - }, - { - "char": 117, - "char_width": 8, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 415 - }, - { - "char": 118, - "char_width": 7, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 5, - "bitmap_offset": 420 - }, - { - "char": 119, - "char_width": 10, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 425 - }, - { - "char": 120, - "char_width": 7, - "x_offset": 2, - "y_offset": 3, - "bitmap_byte_count": 4, - "bitmap_offset": 432 - }, - { - "char": 121, - "char_width": 7, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 7, - "bitmap_offset": 436 - }, - { - "char": 122, - "char_width": 6, - "x_offset": 1, - "y_offset": 3, - "bitmap_byte_count": 4, - "bitmap_offset": 443 - }, - { - "char": 123, - "char_width": 5, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 447 - }, - { - "char": 124, - "char_width": 7, - "x_offset": 4, - "y_offset": 1, - "bitmap_byte_count": 4, - "bitmap_offset": 451 - }, - { - "char": 125, - "char_width": 5, - "x_offset": 1, - "y_offset": 1, - "bitmap_byte_count": 5, - "bitmap_offset": 455 - }, - { - "char": 126, - "char_width": 7, - "x_offset": 2, - "y_offset": 4, - "bitmap_byte_count": 2, - "bitmap_offset": 460 - }, - { - "char": 127, - "char_width": 8, - "x_offset": 2, - "y_offset": 1, - "bitmap_byte_count": 6, - "bitmap_offset": 462 - } - ], - "bagl_font": { - "font_id": 10, - "bpp": 1, - "char_height": 12, - "baseline_height": 9, - "char_leftmost_x": -1, - "first_char": 32, - "last_char": 127 - } - } -] diff --git a/speculos/fonts/stax-fonts-12.bin b/speculos/fonts/stax-fonts-12.bin new file mode 100644 index 00000000..095148c5 Binary files /dev/null and b/speculos/fonts/stax-fonts-12.bin differ diff --git a/speculos/fonts/stax-fonts-13.bin b/speculos/fonts/stax-fonts-13.bin new file mode 120000 index 00000000..4e6e525b --- /dev/null +++ b/speculos/fonts/stax-fonts-13.bin @@ -0,0 +1 @@ +stax-fonts-12.bin \ No newline at end of file diff --git a/speculos/fonts/stax-fonts-14.bin b/speculos/fonts/stax-fonts-14.bin new file mode 100644 index 00000000..ac55d7cf Binary files /dev/null and b/speculos/fonts/stax-fonts-14.bin differ diff --git a/speculos/fonts/stax-fonts-15.bin b/speculos/fonts/stax-fonts-15.bin new file mode 100644 index 00000000..a3f89191 Binary files /dev/null and b/speculos/fonts/stax-fonts-15.bin differ diff --git a/speculos/main.py b/speculos/main.py index 7bf3a3c8..7e200494 100644 --- a/speculos/main.py +++ b/speculos/main.py @@ -7,18 +7,17 @@ import argparse import binascii import ctypes -from elftools.elf.elffile import ELFFile import logging -from mnemonic import mnemonic import os import re import signal import socket import sys import threading - -from distutils.spawn import find_executable import pkg_resources +from elftools.elf.elffile import ELFFile +from mnemonic import mnemonic +from typing import Optional, Type from .api import ApiRunner, EventsBroadcaster from .mcu import apdu as apdu_server @@ -28,7 +27,9 @@ from .mcu.automation_server import AutomationClient, AutomationServer from .mcu.button_tcp import FakeButton from .mcu.finger_tcp import FakeFinger +from .mcu.struct import DisplayArgs, ServerArgs from .mcu.vnc import VNC +from .observer import BroadcastInterface DEFAULT_SEED = ('glory promote mansion idle axis finger extra february uncover one trip resource lawn turtle enact ' @@ -105,11 +106,23 @@ def get_elf_infos(app_path): svc_cx_call_symbol = symtab.get_symbol_by_name("SVC_cx_call") if svc_cx_call_symbol is not None: svc_cx_call_addr = svc_cx_call_symbol[0]['st_value'] & (~1) + # Check where are located fonts in .elf file (LNX/LNS+ only) + # (Stax fonts are loaded at a known location: STAX_FONTS_ARRAY_ADDR) + fonts_addr = 0 + fonts_size = 0 + bagl_fonts_symbol = symtab.get_symbol_by_name('C_bagl_fonts') + if bagl_fonts_symbol is not None: + fonts_addr = bagl_fonts_symbol[0]['st_value'] + fonts_size = bagl_fonts_symbol[0]['st_size'] + logger.info(f"Found C_bagl_fonts at 0x{fonts_addr:X} ({fonts_size} bytes)\n") + else: + logger.info("Disabling OCR.") supp_ram = elf.get_section_by_name('.rfbss') ram_addr, ram_size = (supp_ram['sh_addr'], supp_ram['sh_size']) if supp_ram is not None else (0, 0) stack_size = estack - stack - return sh_offset, sh_size, stack, stack_size, ram_addr, ram_size, text_load_addr, svc_call_addr, svc_cx_call_addr + return sh_offset, sh_size, stack, stack_size, ram_addr, ram_size, text_load_addr, \ + svc_call_addr, svc_cx_call_addr, fonts_addr, fonts_size def get_cx_infos(app_path): @@ -170,7 +183,8 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) -> for lib in [f'main:{app_path}'] + args.library: name, lib_path = lib.split(':') load_offset, load_size, stack, stack_size, ram_addr, ram_size, \ - text_load_addr, svc_call_address, svc_cx_call_address = get_elf_infos(lib_path) + text_load_addr, svc_call_address, svc_cx_call_address, \ + fonts_addr, fonts_size = get_elf_infos(lib_path) # Since binaries loaded as libs could also declare extra RAM page(s), collect them all if (ram_addr, ram_size) != (0, 0): arg = f'{ram_addr:#x}:{ram_size:#x}' @@ -181,6 +195,7 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) -> lib_arg = f'{name}:{lib_path}:{load_offset:#x}:{load_size:#x}' lib_arg += f':{stack:#x}:{stack_size:#x}:{svc_call_address:#x}' lib_arg += f':{svc_cx_call_address:#x}:{text_load_addr:#x}' + lib_arg += f':{fonts_addr:#x}:{fonts_size:#x}' argv.append(lib_arg) if args.model == 'blue': @@ -212,6 +227,11 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) -> if args.deterministic_rng: os.environ['RNG_SEED'] = args.deterministic_rng + if args.user_private_key: + os.environ['USER_PRIVATE_KEY'] = args.user_private_key + if args.attestation_key: + os.environ['ATTESTATION_PRIVATE_KEY'] = args.attestation_key + logger.debug(f"executing qemu: {argv}") try: os.execvp(argv[0], argv) @@ -237,16 +257,7 @@ def setup_logging(args): sys.exit(1) -def main(prog=None): - disable_tesseract = False - if not find_executable("tesseract"): - disable_tesseract = True - warning_message = "\n\n\n!****************************************************************!\n" - warning_message += "tesseract-ocr is not found and is required to run Speculos with ocr.\n" - warning_message += "Please run `sudo apt install tesseract-ocr`\n" - warning_message += "Speculos will continue without tesseract-ocr enabled\n" - warning_message += "!****************************************************************!\n\n\n" - logger.warn(warning_message) +def main(prog=None) -> int: parser = argparse.ArgumentParser(description='Emulate Ledger Nano/Blue apps.') parser.add_argument('app.elf', type=str, help='application path') @@ -254,8 +265,12 @@ def main(prog=None): 'to specify a path') parser.add_argument('--color', default='MATTE_BLACK', choices=list(display.COLORS.keys()), help='Nano color') parser.add_argument('-d', '--debug', action='store_true', help='Wait gdb connection to port 1234') - parser.add_argument('--deterministic-rng', default="", help='Seed the rng with a given value to produce ' + parser.add_argument('--deterministic-rng', default='', help='Seed the rng with a given value to produce ' 'deterministic randomness') + parser.add_argument('--user-private-key', default='', + help='32B in hex format, will be used as the user private keys') + parser.add_argument('--attestation-key', default='', help='32B in hex format, will be used as the private ' + 'attestation key') parser.add_argument('-k', '--sdk', type=str, help='SDK version') parser.add_argument('-a', '--apiLevel', type=str, help='Api level') parser.add_argument('-l', '--library', default=[], action='append', help='Additional library (eg. ' @@ -292,9 +307,6 @@ def main(prog=None): "left button, 'a' right, 's' both). Default: arrow keys") group.add_argument('--progressive', action='store_true', help='Enable step-by-step rendering of graphical elements') group.add_argument('--zoom', help='Display pixel size.', type=int, choices=range(1, 11)) - group.add_argument('--force-full-ocr', action='store_true', - help='Degrade screen display to enhance OCR capacities for inverted text (only for Stax)') - group.add_argument('--disable-tesseract', action='store_true', help='Disable tesseract OCR: only for stax') if prog: parser.prog = prog @@ -412,12 +424,13 @@ def main(prog=None): logger.error("--vnc-password can only be used with --vnc-port") sys.exit(1) + ScreenNotifier: Type[display.DisplayNotifier] if args.display == 'text': - from .mcu.screen_text import TextScreen as Screen + from .mcu.screen_text import TextScreenNotifier as ScreenNotifier elif args.display == 'headless': - from .mcu.headless import Headless as Screen + from .mcu.headless import HeadlessNotifier as ScreenNotifier else: - from .mcu.screen import QtScreen as Screen + from .mcu.screen import QtScreenNotifier as ScreenNotifier if args.sdk and args.apiLevel: logger.error("Either SDK version or api level should be specified") @@ -438,13 +451,15 @@ def main(prog=None): api_enabled = (args.api_port != 0) - automation_path = None + automation_path: Optional[automation.Automation] = None if args.automation: + # TODO: remove this condition and all associated code in next major version logger.warn("--automation is deprecated, please use the REST API instead") automation_path = automation.Automation(args.automation) - automation_server = None + automation_server: Optional[BroadcastInterface] = None if args.automation_port: + # TODO: remove this condition and all associated code in next major version logger.warn("--automation-port is deprecated, please use the REST API instead") if api_enabled: logger.warn("--automation-port is incompatible with the the API server, disabling the latter") @@ -464,11 +479,10 @@ def main(prog=None): apdu = apdu_server.ApduServer(host="0.0.0.0", port=args.apdu_port) seph = seproxyhal.SeProxyHal( s2, + model=args.model, automation=automation_path, automation_server=automation_server, - transport=args.usb, - fonts_path=pkg_resources.resource_filename(__name__, "/fonts"), - api_level=args.apiLevel) + transport=args.usb) button = None if args.button_port: @@ -500,25 +514,31 @@ def main(prog=None): if args.xy: x, y = (int(i) for i in args.xy.split('x')) - apirun = None + apirun: Optional[ApiRunner] = None if api_enabled: apirun = ApiRunner(args.api_port) - if disable_tesseract: - args.disable_tesseract = True - - display_args = display.DisplayArgs(args.color, args.model, args.ontop, rendering, - args.keymap, zoom, x, y, args.force_full_ocr, - args.disable_tesseract) - server_args = display.ServerArgs(apdu, apirun, button, finger, seph, vnc) - screen = Screen(display_args, server_args) - - if api_enabled: - apirun.start_server_thread(screen, seph, automation_server) + display_args = DisplayArgs(args.color, args.model, args.ontop, rendering, + args.keymap, zoom, x, y) + server_args = ServerArgs(apdu, apirun, button, finger, seph, vnc) + screen_notifier = ScreenNotifier(display_args, server_args) - screen.run() + if apirun is not None: + assert automation_server is not None + apirun.start_server_thread(screen_notifier, seph, automation_server) - s2.close() - _, status = os.waitpid(qemu_pid, 0) - qemu_exit_status = os.WEXITSTATUS(status) - return qemu_exit_status + try: + screen_notifier.run() + except BaseException: + # Will deal with exception triggered in the ScreenNotifier, including + # KeyboardInterrupt (if not Qt display, else it will segfault) + logger.exception("An error occurred") + logger.critical("Stopping Speculos") + finally: + if apirun is not None: + apirun.stop() + + s2.close() + _, status = os.waitpid(qemu_pid, 0) + qemu_exit_status = os.WEXITSTATUS(status) + sys.exit(qemu_exit_status) diff --git a/speculos/mcu/apdu.py b/speculos/mcu/apdu.py index 9b122fef..990a0cd0 100644 --- a/speculos/mcu/apdu.py +++ b/speculos/mcu/apdu.py @@ -1,3 +1,5 @@ +from __future__ import annotations + ''' Forward packets between an external application and the emulated device. @@ -8,39 +10,47 @@ import errno import logging import socket +from typing import Optional +from .display import IODevice, DisplayNotifier -class ApduServer: - def __init__(self, host='127.0.0.1', port=9999, hid=False): - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.s.bind((host, port)) # lgtm [py/bind-socket-all-network-interfaces] - self.s.listen() - self.client = None +class ApduServer(IODevice): + def __init__(self, host: str = '127.0.0.1', port: int = 9999, hid: bool = False): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((host, port)) # lgtm [py/bind-socket-all-network-interfaces] + self.socket.listen() + self.client: Optional[ApduClient] = None - def can_read(self, s, screen): - assert self.s.fileno() == s + @property + def file(self): + return self.socket - c, _ = self.s.accept() + def can_read(self, screen: DisplayNotifier): + c, _ = self.file.accept() self.client = ApduClient(c) screen.add_notifier(self.client) - def forward_to_client(self, packet): + def forward_to_client(self, packet: bytes): if self.client is not None: self.client.forward_to_client(packet) -class ApduClient: - def __init__(self, s): - self.s = s +class ApduClient(IODevice): + def __init__(self, sock: socket.socket): + self._socket = sock self.logger = logging.getLogger("apdu") + @property + def file(self): + return self._socket + def _recvall(self, size): data = b'' while size > 0: try: - tmp = self.s.recv(size) + tmp = self.file.recv(size) except ConnectionResetError: tmp = b'' if len(tmp) == 0: @@ -62,19 +72,16 @@ def recv_packet(self): return packet - def can_read(self, s, screen): + def can_read(self, screen: DisplayNotifier) -> None: '''Forward APDU packet to the app''' - - assert self.s.fileno() == s - packet = self.recv_packet() if packet is None: - screen.remove_notifier(self.s.fileno()) - self.s.close() + screen.remove_notifier(self.fileno) + self.file.close() return self.logger.info("> {}".format(packet.hex())) - screen.forward_to_app(packet) + screen.display.forward_to_app(packet) def forward_to_client(self, packet): '''Encode and forward APDU to the client.''' @@ -84,7 +91,7 @@ def forward_to_client(self, packet): size = (len(packet) - 2) & 0xffffffff packet = size.to_bytes(4, 'big') + packet try: - self.s.sendall(packet) + self.file.sendall(packet) except OSError as e: if e.errno == errno.EBADF: # the connection with the client was closed, ignore any error diff --git a/speculos/mcu/automation.py b/speculos/mcu/automation.py index 74a08e73..9a2e6c54 100644 --- a/speculos/mcu/automation.py +++ b/speculos/mcu/automation.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass import json import jsonschema import logging @@ -7,15 +6,6 @@ import re -@dataclass -class TextEvent: - text: str - x: int - y: int - w: int - h: int - - class Automation: def __init__(self, document): self.logger = logging.getLogger("automation") diff --git a/speculos/mcu/automation_server.py b/speculos/mcu/automation_server.py index 21c49d90..add55d41 100644 --- a/speculos/mcu/automation_server.py +++ b/speculos/mcu/automation_server.py @@ -8,39 +8,35 @@ import threading from typing import List from dataclasses import asdict -from .automation import TextEvent +from speculos.observer import BroadcastInterface, ObserverInterface, TextEvent -class AutomationServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + +class AutomationServer(socketserver.ThreadingMixIn, socketserver.TCPServer, BroadcastInterface): daemon_threads = True allow_reuse_address = True def __init__(self, server_address, RequestHandlerClass, *args, **kwargs): + BroadcastInterface.__init__(self) socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass) - self.logger = logging.getLogger("automation") - self.clients = [] - - def add_client(self, client): - self.clients.append(client) - - def remove_client(self, client): - self.clients.remove(client) def broadcast(self, event: TextEvent): """Broadcast an event to each connected client.""" - self.logger.debug(f"broadcast {asdict(event)} to {self.clients}") + self.logger.debug("Broadcast %s to %s", asdict(event), self.clients) for client in self.clients: client.send_screen_event(event) -class AutomationClient(socketserver.BaseRequestHandler): - def setup(self): +class AutomationClient(socketserver.BaseRequestHandler, ObserverInterface): + + def setup(self) -> None: + self.server: AutomationServer self.logger = logging.getLogger("automation") self.condition = threading.Condition() self.events: List[TextEvent] = [] - self.logger.debug(f"new client from {self.client_address}") + self.logger.debug("New client from %s", self.client_address) self.server.add_client(self) def handle(self): @@ -58,10 +54,10 @@ def handle(self): return def finish(self): - self.logger.debug("connection closed with client") + self.logger.debug("Connection closed with client %s", self) self.server.remove_client(self) - def send_screen_event(self, event): + def send_screen_event(self, event: TextEvent) -> None: self.events.append(event) with self.condition: self.condition.notify() diff --git a/speculos/mcu/bagl.py b/speculos/mcu/bagl.py index 5388f3be..2ed5b48e 100644 --- a/speculos/mcu/bagl.py +++ b/speculos/mcu/bagl.py @@ -1,12 +1,13 @@ import binascii import logging -from typing import List from collections import namedtuple from construct import Aligned, Struct, Int8ul, Int16ul, Int32ul, Padded +from typing import List, Optional, Tuple +from speculos.observer import TextEvent from . import bagl_font from . import bagl_glyph -from .automation import TextEvent +from .display import FrameBuffer, GraphicLibrary bagl_component_t = Aligned(4, Struct( "type" / Int8ul, @@ -65,17 +66,24 @@ DrawState = namedtuple('DrawState', 'x y width height colors bpp xx yy') -class Bagl: - def __init__(self, m, size): - self.m = m - self.SCREEN_WIDTH, self.SCREEN_HEIGHT = size +class Bagl(GraphicLibrary): + def __init__(self, fb: FrameBuffer, size: Tuple[int, int], model: str): + super().__init__(fb, size, model) self.draw_state = DrawState(0, 0, 0, 0, [], 0, 0, 0) - self.logger = logging.getLogger("bagl") - - def refresh(self) -> bool: - return self.m.update() - - def hal_draw_bitmap_within_rect(self, x, y, width, height, colors, bpp, bitmap, restore=None): + self.logger = logging.getLogger("BAGL") + + def refresh(self, _: Optional[bytes] = None) -> bool: + return self.fb.update() + + def hal_draw_bitmap_within_rect(self, + x: int, + y: int, + width: int, + height: int, + colors: List[int], + bpp: int, + bitmap: List[int], + restore: Optional[Tuple[int, int]] = None) -> None: if bpp == 3 or bpp > 4: return @@ -99,7 +107,7 @@ def hal_draw_bitmap_within_rect(self, x, y, width, height, colors, bpp, bitmap, xx, yy = restore bitmap = list(bitmap) - while bitmap or yy < requested_yh: + while bitmap or (yy < requested_yh and self.model != "blue"): if bitmap: ch = bitmap.pop(0) else: @@ -109,7 +117,7 @@ def hal_draw_bitmap_within_rect(self, x, y, width, height, colors, bpp, bitmap, if xx >= 0 and xx < self.SCREEN_WIDTH and yy >= 0 and yy < self.SCREEN_HEIGHT: pixel_color_index = (ch >> i) & pixel_mask color = colors[pixel_color_index] - self.m.draw_point(xx, yy, color) + self.fb.draw_point(xx, yy, color) xx += 1 if xx >= requested_xw: @@ -121,7 +129,7 @@ def hal_draw_bitmap_within_rect(self, x, y, width, height, colors, bpp, bitmap, self.draw_state = DrawState(x, y, width, height, colors, bpp, xx, yy) - def hal_draw_rect(self, color, x, y, width, height): + def _hal_draw_rect(self, color: int, x: int, y: int, width: int, height: int) -> None: if x + width > self.SCREEN_WIDTH or x < 0: if x > self.SCREEN_WIDTH: return @@ -141,7 +149,7 @@ def hal_draw_rect(self, color, x, y, width, height): i = width * height while i > 0: i -= 1 - self.m.draw_point(xx, y, color) + self.fb.draw_point(xx, y, color) YX += 1 xx += 1 if YX >= YXlinemax: @@ -154,7 +162,7 @@ def hal_draw_rect(self, color, x, y, width, height): break @staticmethod - def compute_line_width(font_id, width, text, text_encoding): + def _compute_line_width(font_id: int, width: int, text: bytes) -> int: font = bagl_font.get(font_id) if not font: logging.error("font not found") @@ -191,10 +199,18 @@ def compute_line_width(font_id, width, text, text_encoding): return xx - def draw_string(self, font_id, fgcolor, bgcolor, x, y, width, height, text, text_encoding): + def _draw_string(self, + font_id: int, + fgcolor: int, + bgcolor: int, + x: int, + y: int, + width: int, + height: int, + text: bytes) -> int: font = bagl_font.get(font_id) if not font: - self.logger.error("unsupported font {}".format(font_id & bagl_font.BAGL_FONT_ID_MASK)) + self.logger.error("unsupported font %d", font_id & bagl_font.BAGL_FONT_ID_MASK) return 0 if font.bpp > 1: @@ -228,11 +244,11 @@ def draw_string(self, font_id, fgcolor, bgcolor, x, y, width, height, text, text text = text.replace(b'\r\n', b'\n') for ch in text: - ch_height = font.char_height - ch_kerning = 0 - ch_width = 0 - ch_bitmap = None - ch_y = y + ch_height: int = font.char_height + ch_kerning: int = 0 + ch_width: int = 0 + ch_bitmap: Optional[List[int]] = None + ch_y: int = y if ch < font.first_char or ch > font.last_char: if ch in ['\r', '\n']: @@ -271,13 +287,20 @@ def draw_string(self, font_id, fgcolor, bgcolor, x, y, width, height, text, text if ch_bitmap: self.hal_draw_bitmap_within_rect(xx, ch_y, ch_width, ch_height, colors, font.bpp, ch_bitmap) else: - self.hal_draw_rect(bgcolor, xx, ch_y, ch_width, ch_height) + self._hal_draw_rect(bgcolor, xx, ch_y, ch_width, ch_height) xx += ch_width + ch_kerning return (y << 16) | (xx & 0xFFFF) - def _draw_circle_helper(self, color, x_center, y_center, radius, octants, radiusint, colorint): + def _draw_circle_helper(self, + color: int, + x_center: int, + y_center: int, + radius: int, + octants: int, + radiusint: int, + colorint: int) -> None: x, y = radius, 0 decisionOver2 = 1 - x dradius = radius-radiusint @@ -287,56 +310,56 @@ def _draw_circle_helper(self, color, x_center, y_center, radius, octants, radius while y <= x: if octants & 1: if drawint: - self.hal_draw_rect(colorint, x_center, y+y_center, x-(dradius-1), 1) - self.hal_draw_rect(color, x_center+x-(dradius-1), y+y_center, dradius, 1) + self._hal_draw_rect(colorint, x_center, y+y_center, x-(dradius-1), 1) + self._hal_draw_rect(color, x_center+x-(dradius-1), y+y_center, dradius, 1) else: - self.hal_draw_rect(color, x_center, y+y_center-1, x, 1) + self._hal_draw_rect(color, x_center, y+y_center-1, x, 1) if octants & 2: if drawint: if last_x != x: - self.hal_draw_rect(colorint, x_center, x+y_center, y-(dradius-1), 1) - self.hal_draw_rect(color, x_center+y-(dradius-1), x+y_center, dradius, 1) + self._hal_draw_rect(colorint, x_center, x+y_center, y-(dradius-1), 1) + self._hal_draw_rect(color, x_center+y-(dradius-1), x+y_center, dradius, 1) else: - self.hal_draw_rect(color, x_center, x+y_center-1, y, 1) + self._hal_draw_rect(color, x_center, x+y_center-1, y, 1) if octants & 4: if drawint: - self.hal_draw_rect(colorint, x_center-x, y+y_center, x-(dradius-1), 1) - self.hal_draw_rect(color, x_center-x-(dradius-1), y+y_center, dradius, 1) + self._hal_draw_rect(colorint, x_center-x, y+y_center, x-(dradius-1), 1) + self._hal_draw_rect(color, x_center-x-(dradius-1), y+y_center, dradius, 1) else: - self.hal_draw_rect(color, x_center-x, y+y_center-1, x, 1) + self._hal_draw_rect(color, x_center-x, y+y_center-1, x, 1) if octants & 8: if drawint: if last_x != x: - self.hal_draw_rect(colorint, x_center-y, x+y_center, y-(dradius-1), 1) - self.hal_draw_rect(color, x_center-y-(dradius-1), x+y_center, dradius, 1) + self._hal_draw_rect(colorint, x_center-y, x+y_center, y-(dradius-1), 1) + self._hal_draw_rect(color, x_center-y-(dradius-1), x+y_center, dradius, 1) else: - self.hal_draw_rect(color, x_center-y, x+y_center-1, y, 1) + self._hal_draw_rect(color, x_center-y, x+y_center-1, y, 1) if octants & 16: if drawint: - self.hal_draw_rect(colorint, x_center, y_center-y, x-(dradius-1), 1) - self.hal_draw_rect(color, x_center+x-(dradius-1), y_center-y, dradius, 1) + self._hal_draw_rect(colorint, x_center, y_center-y, x-(dradius-1), 1) + self._hal_draw_rect(color, x_center+x-(dradius-1), y_center-y, dradius, 1) else: - self.hal_draw_rect(color, x_center, y_center-y, x, 1) + self._hal_draw_rect(color, x_center, y_center-y, x, 1) if octants & 32: if drawint: if last_x != x: - self.hal_draw_rect(colorint, x_center, y_center-x, y-(dradius-1), 1) - self.hal_draw_rect(color, x_center+y-(dradius-1), y_center-x, dradius, 1) + self._hal_draw_rect(colorint, x_center, y_center-x, y-(dradius-1), 1) + self._hal_draw_rect(color, x_center+y-(dradius-1), y_center-x, dradius, 1) else: - self.hal_draw_rect(color, x_center, y_center-x, y, 1) + self._hal_draw_rect(color, x_center, y_center-x, y, 1) if octants & 64: if drawint: - self.hal_draw_rect(colorint, x_center-x, y_center-y, x-(dradius-1), 1) - self.hal_draw_rect(color, x_center-x-(dradius-1), y_center-y, dradius, 1) + self._hal_draw_rect(colorint, x_center-x, y_center-y, x-(dradius-1), 1) + self._hal_draw_rect(color, x_center-x-(dradius-1), y_center-y, dradius, 1) else: - self.hal_draw_rect(color, x_center-x, y_center-y, x, 1) + self._hal_draw_rect(color, x_center-x, y_center-y, x, 1) if octants & 128: if drawint: if last_x != x: - self.hal_draw_rect(colorint, x_center-y, y_center-x, y-(dradius-1), 1) - self.hal_draw_rect(color, x_center-y-(dradius-1), y_center-x, dradius, 1) + self._hal_draw_rect(colorint, x_center-y, y_center-x, y-(dradius-1), 1) + self._hal_draw_rect(color, x_center-y-(dradius-1), y_center-x, dradius, 1) else: - self.hal_draw_rect(color, x_center-y, y_center-x, y, 1) + self._hal_draw_rect(color, x_center-y, y_center-x, y, 1) last_x = x y += 1 @@ -346,12 +369,12 @@ def _draw_circle_helper(self, color, x_center, y_center, radius, octants, radius x -= 1 decisionOver2 += 2 * (y - x) + 1 - def _display_bagl_icon(self, component, context): + def _display_bagl_icon(self, component, context: bytes) -> None: if component.icon_id != 0: - self.logger.debug(f"icon_id {component.icon_id}") + self.logger.debug("icon_id %d", component.icon_id) glyph = bagl_glyph.get(component.icon_id) if not glyph: - self.logger.error(f"glyph {component.icon_id:#x} not found") + self.logger.error("glyph %#x not found", component.icon_id) return if len(context) != 0: @@ -373,7 +396,7 @@ def _display_bagl_icon(self, component, context): glyph.bitmap) else: if len(context) == 0: - self.logger.info("len context == 0 {}".format(binascii.hexlify(context))) + self.logger.info("len context == 0 %s", binascii.hexlify(context)) return bpp = context[0] @@ -385,7 +408,7 @@ def _display_bagl_icon(self, component, context): color = int.from_bytes(context[n:n+4], byteorder='big') colors.append(color) n += 4 - bitmap = context[n:] + bitmap = list(context[n:]) # converting bytes to list bitmap_length_bits = bpp * component.width * component.height assert len(bitmap) * 8 >= bitmap_length_bits self.hal_draw_bitmap_within_rect(component.x, component.y, @@ -406,7 +429,7 @@ def _display_bagl_rectangle(self, component, context, context_encoding, halignme (component.x+component.width-radius-1, component.y+radius, radius, component.height-2*radius), ] for (x, y, width, height) in coords: - self.hal_draw_rect(component.bgcolor, x, y, width, height) + self._hal_draw_rect(component.bgcolor, x, y, width, height) coords = [ # outline. 4 rectangles (with last pixel of each corner not set) # top, bottom, left, right @@ -416,7 +439,7 @@ def _display_bagl_rectangle(self, component, context, context_encoding, halignme (component.x+component.width-1, component.y+radius, component.stroke, component.height-2*radius), ] for (x, y, width, height) in coords: - self.hal_draw_rect(component.fgcolor, x, y, width, height) + self._hal_draw_rect(component.fgcolor, x, y, width, height) else: coords = [ # centered top to bottom @@ -427,7 +450,7 @@ def _display_bagl_rectangle(self, component, context, context_encoding, halignme (component.x+component.width-radius, component.y+radius, radius, component.height-2*radius), ] for (x, y, width, height) in coords: - self.hal_draw_rect(component.fgcolor, x, y, width, height) + self._hal_draw_rect(component.fgcolor, x, y, width, height) if radius > 1: radiusint = 0 @@ -467,15 +490,14 @@ def _display_bagl_rectangle(self, component, context, context_encoding, halignme if component.fill == BAGL_FILL: fgcolor, bgcolor = bgcolor, fgcolor stroke = max(1, component.stroke * 2) - self.draw_string(component.font_id, - fgcolor, - bgcolor, - component.x + halignment, # XXX: take icon_width into account - component.y + valignment, - component.width - halignment - stroke, # XXX: take icon_width into account - component.height - valignment - stroke, - context, - context_encoding) + self._draw_string(component.font_id, + fgcolor, + bgcolor, + component.x + halignment, # XXX: take icon_width into account + component.y + valignment, + component.width - halignment - stroke, # XXX: take icon_width into account + component.height - valignment - stroke, + context) def _display_bagl_labeline(self, component, @@ -492,15 +514,14 @@ def _display_bagl_labeline(self, if type_ == BAGL_LABELINE: y -= baseline height = char_height - self.hal_draw_rect(component.bgcolor, component.x, y, halignment, height) - self.hal_draw_rect(component.bgcolor, component.x + halignment + strwidth, - y, component.width - (halignment + strwidth), height) + self._hal_draw_rect(component.bgcolor, component.x, y, halignment, height) + self._hal_draw_rect(component.bgcolor, component.x + halignment + strwidth, + y, component.width - (halignment + strwidth), height) if len(text) == 0: return [] # XXX - context_encoding = 0 y = component.y height = component.height if type_ == BAGL_LABELINE: @@ -508,15 +529,14 @@ def _display_bagl_labeline(self, else: y += valignment height += valignment - self.draw_string(component.font_id, - component.fgcolor, - component.bgcolor, - component.x + halignment, - y, - component.width - halignment, - component.height, - text, - context_encoding) + self._draw_string(component.font_id, + component.fgcolor, + component.bgcolor, + component.x + halignment, + y, + component.width - halignment, + component.height, + text) return [TextEvent(text.decode("utf-8", "ignore"), component.x + halignment, y, @@ -538,10 +558,9 @@ def _display_get_alignment(self, component, context, context_encoding): char_height = font.char_height if context: - strwidth = Bagl.compute_line_width(component.font_id, - component.width + 100, - context, - context_encoding) + strwidth = self._compute_line_width(component.font_id, + component.width + 100, + context) haligned = (component.font_id & BAGL_FONT_ALIGNMENT_HORIZONTAL_MASK) if haligned == BAGL_FONT_ALIGNMENT_RIGHT: @@ -565,11 +584,11 @@ def _display_get_alignment(self, component, context, context_encoding): return (halignment, valignment, baseline, char_height, strwidth) - def display_status(self, data) -> List[TextEvent]: + def display_status(self, data: bytes) -> List[TextEvent]: component = bagl_component_t.parse(data) context = data[bagl_component_t.sizeof():] context_encoding = 0 # XXX - self.logger.debug("component: {}".format(component)) + self.logger.debug("component: %s", component) ret = self._display_get_alignment(component, context, context_encoding) (halignment, valignment, baseline, char_height, strwidth) = ret @@ -592,22 +611,23 @@ def display_status(self, data) -> List[TextEvent]: self._display_bagl_icon(component, context) return ret - def display_raw_status(self, data): + def display_raw_status(self, data: bytes) -> None: if data[0] == SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START: x = int.from_bytes(data[1:3], byteorder='big', signed=True) y = int.from_bytes(data[3:5], byteorder='big', signed=True) w = int.from_bytes(data[5:7], byteorder='big') h = int.from_bytes(data[7:9], byteorder='big') bpp = int.from_bytes(data[9:10], byteorder='big') + # character = data[10:14] # Added by speculos syscall (unused here) color_size = 4 * (1 << bpp) colors = [] - for i in range(10, 10 + color_size, 4): + for i in range(14, 14 + color_size, 4): color = int.from_bytes(data[i:i+4], byteorder='little') colors.append(color) - bitmap = data[10+color_size:] + bitmap = data[14+color_size:] - self.hal_draw_bitmap_within_rect(x, y, w, h, colors, bpp, bitmap) + self.hal_draw_bitmap_within_rect(x, y, w, h, colors, bpp, list(bitmap)) else: bitmap = data[1:] @@ -620,4 +640,4 @@ def display_raw_status(self, data): bpp = self.draw_state.bpp restore = (self.draw_state.xx, self.draw_state.yy) - self.hal_draw_bitmap_within_rect(x, y, w, h, colors, bpp, bitmap, restore) + self.hal_draw_bitmap_within_rect(x, y, w, h, colors, bpp, list(bitmap), restore) diff --git a/speculos/mcu/bagl_font.py b/speculos/mcu/bagl_font.py index 2fbfb81a..02134dd2 100644 --- a/speculos/mcu/bagl_font.py +++ b/speculos/mcu/bagl_font.py @@ -1,4 +1,5 @@ from collections import namedtuple +from typing import Optional BAGL_FONT_ID_MASK = 0x0fff @@ -43,8 +44,7 @@ } bagl_font_character_t; ''' -Font = namedtuple( - "Font", "font_id font_name bpp char_height baseline_height char_kerning first_char last_char characters bitmap") +Font = namedtuple("Font", "font_id bpp char_height baseline_height char_kerning first_char last_char characters bitmap") FontCharacter = namedtuple("FontCharacter", "char_width bitmap_byte_count bitmap_offset") bitmapLUCIDA_CONSOLE_6PT_8H = [ @@ -3061,93 +3061,34 @@ ] FONTS = [ - Font( - BAGL_FONT_OPEN_SANS_REGULAR_11px, - "bagl_font_open_sans_regular_11px", - 1, 12, 9, 0, - 0x0020, 0x007F, - charactersOPEN_SANS_REGULAR_11PX, - bitmapOPEN_SANS_REGULAR_11PX), - Font( - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, - "bagl_font_open_sans_extrabold_11px", - 1, 12, 9, 0, 0x0020, 0x007F, - charactersOPEN_SANS_EXTRABOLD_11PX, - bitmapOPEN_SANS_EXTRABOLD_11PX), - Font( - BAGL_FONT_OPEN_SANS_LIGHT_16px, - "bagl_font_open_sans_light_16px", - 1, 18, 13, 0, - 0x0020, 0x007F, - charactersOPEN_SANS_LIGHT_16PX, - bitmapOPEN_SANS_LIGHT_16PX), - Font( - BAGL_FONT_LUCIDA_CONSOLE_8PX, - None, - 1, 8, 16, 0, - 0x0020, 0x00ff, - charactersLUCIDA_CONSOLE_6PT_8H, - bitmapLUCIDA_CONSOLE_6PT_8H), - Font( - BAGL_FONT_OPEN_SANS_LIGHT_16_22PX, - None, - 4, 22, 16, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_LIGHT_16_22PX, - bitmapOPEN_SANS_LIGHT_16_22PX), - Font( - BAGL_FONT_OPEN_SANS_REGULAR_8_11PX, - None, - 4, 11, 8, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_REGULAR_8_11PX, - bitmapOPEN_SANS_REGULAR_8_11PX), - Font( - BAGL_FONT_OPEN_SANS_REGULAR_10_13PX, - None, - 4, 14, 10, 0, - 0x0020, 0x007F, - charactersOPEN_SANS_REGULAR_10_13PX, - bitmapOPEN_SANS_REGULAR_10_13PX), - Font( - BAGL_FONT_OPEN_SANS_SEMIBOLD_10_13PX, - None, - 4, 14, 10, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_SEMIBOLD_10_13PX, - bitmapOPEN_SANS_SEMIBOLD_10_13PX), - Font( - BAGL_FONT_OPEN_SANS_SEMIBOLD_8_11PX, - None, - 4, 11, 8, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_SEMIBOLD_8_11PX, - bitmapOPEN_SANS_SEMIBOLD_8_11PX), - Font( - BAGL_FONT_OPEN_SANS_REGULAR_11_14PX, - None, - 4, 16, 12, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_REGULAR_11_14PX, - bitmapOPEN_SANS_REGULAR_11_14PX), - Font( - BAGL_FONT_OPEN_SANS_SEMIBOLD_11_16PX, - None, - 4, 16, 12, 0, - 0x0020, 0x007f, - charactersOPEN_SANS_SEMIBOLD_11_16PX, - bitmapOPEN_SANS_SEMIBOLD_11_16PX), - Font( - BAGL_FONT_SYMBOLS_0, - None, - 4, 16, 16, 0, - 0x0000, 0x0006, - charactersSYMBOLS_0, - bitmapSYMBOLS_0) + Font(BAGL_FONT_LUCIDA_CONSOLE_8PX, 1, 8, 16, 0, 0x0020, 0x00ff, + charactersLUCIDA_CONSOLE_6PT_8H, bitmapLUCIDA_CONSOLE_6PT_8H), + Font(BAGL_FONT_OPEN_SANS_LIGHT_16_22PX, 4, 22, 16, 0, 0x0020, 0x007f, + charactersOPEN_SANS_LIGHT_16_22PX, bitmapOPEN_SANS_LIGHT_16_22PX), + Font(BAGL_FONT_OPEN_SANS_REGULAR_8_11PX, 4, 11, 8, 0, 0x0020, 0x007f, + charactersOPEN_SANS_REGULAR_8_11PX, bitmapOPEN_SANS_REGULAR_8_11PX), + Font(BAGL_FONT_OPEN_SANS_REGULAR_10_13PX, 4, 14, 10, 0, 0x0020, 0x007F, + charactersOPEN_SANS_REGULAR_10_13PX, bitmapOPEN_SANS_REGULAR_10_13PX), + Font(BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, 1, 12, 9, 0, 0x0020, 0x007F, + charactersOPEN_SANS_EXTRABOLD_11PX, bitmapOPEN_SANS_EXTRABOLD_11PX), + Font(BAGL_FONT_OPEN_SANS_REGULAR_11px, 1, 12, 9, 0, 0x0020, 0x007F, + charactersOPEN_SANS_REGULAR_11PX, bitmapOPEN_SANS_REGULAR_11PX), + Font(BAGL_FONT_OPEN_SANS_LIGHT_16px, 1, 18, 13, 0, 0x0020, 0x007F, + charactersOPEN_SANS_LIGHT_16PX, bitmapOPEN_SANS_LIGHT_16PX), + Font(BAGL_FONT_OPEN_SANS_SEMIBOLD_10_13PX, 4, 14, 10, 0, 0x0020, 0x007f, + charactersOPEN_SANS_SEMIBOLD_10_13PX, bitmapOPEN_SANS_SEMIBOLD_10_13PX), + Font(BAGL_FONT_OPEN_SANS_SEMIBOLD_8_11PX, 4, 11, 8, 0, 0x0020, 0x007f, + charactersOPEN_SANS_SEMIBOLD_8_11PX, bitmapOPEN_SANS_SEMIBOLD_8_11PX), + Font(BAGL_FONT_OPEN_SANS_REGULAR_11_14PX, 4, 16, 12, 0, 0x0020, 0x007f, + charactersOPEN_SANS_REGULAR_11_14PX, bitmapOPEN_SANS_REGULAR_11_14PX), + Font(BAGL_FONT_OPEN_SANS_SEMIBOLD_11_16PX, 4, 16, 12, 0, 0x0020, 0x007f, + charactersOPEN_SANS_SEMIBOLD_11_16PX, bitmapOPEN_SANS_SEMIBOLD_11_16PX), + Font(BAGL_FONT_SYMBOLS_0, 4, 16, 16, 0, 0x0000, 0x0006, + charactersSYMBOLS_0, bitmapSYMBOLS_0) ] -def get(font_id): +def get(font_id: int) -> Optional[Font]: font_id &= BAGL_FONT_ID_MASK for font in FONTS: if font.font_id == font_id: diff --git a/speculos/mcu/button_tcp.py b/speculos/mcu/button_tcp.py index 027480cb..0a78da46 100644 --- a/speculos/mcu/button_tcp.py +++ b/speculos/mcu/button_tcp.py @@ -14,8 +14,10 @@ import socket import time +from .display import DisplayNotifier, IODevice -class FakeButtonClient: + +class FakeButtonClient(IODevice): actions = { 'L': (1, True), 'l': (1, False), @@ -23,18 +25,22 @@ class FakeButtonClient: 'r': (2, False), } - def __init__(self, s): - self.s = s + def __init__(self, sock: socket.socket): + self.socket = sock self.logger = logging.getLogger("button") - def _close(self, screen): - screen.remove_notifier(self.s.fileno()) + @property + def file(self): + return self.socket + + def _cleanup(self, screen: DisplayNotifier): + screen.remove_notifier(self.fileno) self.logger.debug("connection closed with fake button client") - def can_read(self, s, screen): - packet = self.s.recv(1) + def can_read(self, screen: DisplayNotifier): + packet = self.file.recv(1) if packet == b'': - self._close(screen) + self._cleanup(screen) return for c in packet: @@ -42,22 +48,26 @@ def can_read(self, s, screen): if c in self.actions.keys(): key, pressed = self.actions[c] self.logger.debug(f"button {key} release: {pressed}") - screen.seph.handle_button(key, pressed) + screen.display.seph.handle_button(key, pressed) time.sleep(0.1) else: self.logger.debug(f"ignoring byte {c!r}") -class FakeButton: - def __init__(self, port): - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.s.bind(('0.0.0.0', port)) # lgtm [py/bind-socket-all-network-interfaces] - self.s.listen(5) +class FakeButton(IODevice): + def __init__(self, port: int): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(('0.0.0.0', port)) # lgtm [py/bind-socket-all-network-interfaces] + self.socket.listen(5) self.logger = logging.getLogger("button") - def can_read(self, s, screen): - c, addr = self.s.accept() - self.logger.debug(f"new client from {addr}") + @property + def file(self): + return self.socket + + def can_read(self, screen: DisplayNotifier): + c, addr = self.file.accept() + self.logger.debug("New client from %s", addr) client = FakeButtonClient(c) screen.add_notifier(client) diff --git a/speculos/mcu/display.py b/speculos/mcu/display.py index c4685a9f..7ecb39ef 100644 --- a/speculos/mcu/display.py +++ b/speculos/mcu/display.py @@ -1,74 +1,74 @@ +from __future__ import annotations + import io from abc import ABC, abstractmethod -from collections import namedtuple -from typing import Dict, Union +try: + from functools import cache +except ImportError: + # `functools.cache` does not exists on Python3.8 + from functools import lru_cache + cache = lru_cache(maxsize=None) from PIL import Image +from socket import socket +from typing import Any, Dict, IO, List, Optional, Tuple, Union -from .apdu import ApduServer -from .seproxyhal import SeProxyHal -from .button_tcp import FakeButton -from .finger_tcp import FakeFinger -from .vnc import VNC - -Server = Union[ApduServer, FakeButton, FakeFinger, SeProxyHal, VNC] - -DisplayArgs = namedtuple("DisplayArgs", "color model ontop rendering keymap pixel_size x y force_full_ocr, \ - disable_tesseract") -ServerArgs = namedtuple("ServerArgs", "apdu apirun button finger seph vnc") - -Model = namedtuple('Model', 'name screen_size box_position box_size') -MODELS = { - 'nanos': Model('Nano S', (128, 32), (20, 13), (100, 26)), - 'nanox': Model('Nano X', (128, 64), (5, 5), (10, 10)), - 'nanosp': Model('Nano SP', (128, 64), (5, 5), (10, 10)), - 'blue': Model('Blue', (320, 480), (13, 13), (26, 26)), - 'stax': Model('Stax', (400, 672), (13, 13), (26, 26)), -} +from speculos.observer import TextEvent +from .struct import DisplayArgs, MODELS, Pixel, ServerArgs + +PixelColorMapping = Dict[Pixel, int] + + +class IODevice(ABC): + """ + An interface every class implementing application IOs (screens, buttons, APDUs, ...) should + inherit from. + + These classes will be managed by a `DisplayNotifier`, which is responsible to redirect events + to the correct `IODevice` instance through its `IODevice.can_read` callback. + """ + + @property + @abstractmethod + def file(self) -> Union[IO[bytes], socket]: + """ + Returns the file (Pipe, Socket, ...) tied to this `IODevice` + """ + raise NotImplementedError() -COLORS = { + @property + def fileno(self) -> int: + """ + Returns the file descriptor of the file tied to this `IODevice` + """ + return self.file.fileno() + + @abstractmethod + def can_read(self, screen: DisplayNotifier) -> None: + """ + Callback used by a notifier to trigger `IODevice` events on given screen + """ + pass + + +COLORS: Dict[str, int] = { 'LAGOON_BLUE': 0x7ebab5, 'JADE_GREEN': 0xb9ceac, 'FLAMINGO_PINK': 0xd8a0a6, 'SAFFRON_YELLOW': 0xf6a950, 'MATTE_BLACK': 0x111111, - 'CHARLOTTE_PINK': 0xff5555, 'ARNAUD_GREEN': 0x79ff79, 'SYLVE_CYAN': 0x29f3f3, } -def _screenshot_to_iobytes_value(screen_size, data): - image = Image.frombytes("RGB", screen_size, data) - iobytes = io.BytesIO() - image.save(iobytes, format="PNG") - return iobytes.getvalue() - - -class Screenshot: - def __init__(self, screen_size): - self.pixels = {} - self.width, self.height = screen_size - for y in range(0, self.height): - for x in range(0, self.width): - self.pixels[(x, y)] = 0x000000 +class FrameBuffer: + """ + A class responsible for managing the graphic screen of the current application. - def update(self, pixels): - # Don't call update, replace the object instead - self.pixels = {**self.pixels, **pixels} + It updates the screen, takes screenshots, manages colors and such. + """ - def get_image(self): - # Get the pixels object once, as it may be replaced during the loop. - pixels = self.pixels - data = bytearray(self.width * self.height * 3) - for y in range(0, self.height): - for x in range(0, self.width): - pos = 3 * (y * self.width + x) - data[pos:pos + 3] = pixels[(x, y)].to_bytes(3, "big") - return (self.width, self.height), bytes(data) - - -class FrameBuffer(ABC): COLORS = { "nanos": 0x00fffb, "nanox": 0xdddddd, @@ -76,95 +76,242 @@ class FrameBuffer(ABC): "stax": 0xdddddd, } - def __init__(self, model): - self.pixels = {} + def __init__(self, model: str): + self.pixels: PixelColorMapping = {} + self.screenshot_pixels: PixelColorMapping = {} + self.default_color = 0 + self.draw_default_color = False + self._public_screenshot_value = b'' + self.current_data = b'' + self.recreate_public_screenshot = True self.model = model - self.screenshot = Screenshot(MODELS[model].screen_size) - # Init published content now, don't wait for the first request - if self.model == "stax": - self.update_public_screenshot() + self.current_screen_size = MODELS[model].screen_size + self._width, self._height = MODELS[model].screen_size - def draw_point(self, x, y, color): + @cache + def check_color(self, color: int) -> int: # There are only 2 colors on the Nano S and the Nano X but the one # passed in argument isn't always valid. Fix it here. if self.model != 'stax': if color != 0x000000: color = FrameBuffer.COLORS.get(self.model, color) - self.pixels[(x, y)] = color + return color + + def draw_point(self, x: int, y: int, color: int) -> None: + self.pixels[(x, y)] = self.check_color(color) + + def draw_horizontal_line(self, x0: int, y: int, width: int, color: int) -> None: + for x in range(x0, x0 + width): + self.pixels[(x, y)] = self.check_color(color) + + def draw_rect(self, x0: int, y0: int, width: int, height: int, color: int) -> None: + color = self.check_color(color) + + if x0 == 0 and y0 == 0 and width == self._width and height == self._height: + self.default_color = color + self.draw_default_color = True + self.pixels = {} + self.screenshot_pixels = {} + return - def screenshot_update_pixels(self): - # Update the screenshot object with our current pixels content - self.screenshot.update(self.pixels) + for x in range(x0, x0 + width): + for y in range(y0, y0 + height): + self.pixels[(x, y)] = color - def take_screenshot(self): - self.current_screen_size, self.current_data = self.screenshot.get_image() - return self.current_screen_size, self.current_data + def _get_image(self) -> bytes: + data = bytearray(self.default_color.to_bytes(3, "big")) * self._width * self._height + for (x, y), color in self.screenshot_pixels.items(): + pos = 3 * (y * self._width + x) + data[pos:pos + 3] = color.to_bytes(3, "big") + return bytes(data) - def update_public_screenshot(self): + def _get_screenshot_iobytes_value(self) -> bytes: + # Get the pixels object once, as it may be replaced during the loop. + data = self._get_image() + + image = Image.frombytes("RGB", self.current_screen_size, data) + iobytes = io.BytesIO() + image.save(iobytes, format="PNG") + return iobytes.getvalue() + + def take_screenshot(self) -> Tuple[Tuple[int, int], bytes]: + return self.current_screen_size, self._get_image() + + def update_screenshot(self) -> None: + self.screenshot_pixels.update(self.pixels) + + def update_public_screenshot(self) -> None: # Stax only # As we lazyly evaluate the published screenshot, we only flag the evaluation update as necessary self.recreate_public_screenshot = True @property - def public_screenshot_value(self): + def public_screenshot_value(self) -> bytes: # Stax only # Lazy calculation of the published screenshot, as it is a costly operation # and not necessary if no one tries to read the value if self.recreate_public_screenshot: self.recreate_public_screenshot = False - self._public_screenshot_value = _screenshot_to_iobytes_value(self.current_screen_size, self.current_data) + self._public_screenshot_value = self._get_screenshot_iobytes_value() return self._public_screenshot_value - def get_public_screenshot(self): + def get_public_screenshot(self) -> bytes: if self.model == "stax": # On Stax, we only make the screenshot public on the RESTFUL api when it is consistent with events # On top of this, take_screenshot is time consuming on stax, so we'll do as few as possible # We return the value calculated last time update_public_screenshot was called return self.public_screenshot_value - else: - # On nano we have no knowledge of screen refreshes so we can't be scarce on publishes - # So we publish the raw current content every time. It's ok as take_screenshot is fast on Nano - screen_size, data = self.take_screenshot() - return _screenshot_to_iobytes_value(screen_size, data) + # On nano we have no knowledge of screen refreshes so we can't be scarce on publishes + # So we publish the raw current content every time. It's ok as take_screenshot is fast on Nano + return self._get_screenshot_iobytes_value() + + # Should be declared as an `@abstractmethod` (and also `class FrameBuffer(ABC):`), but in this + # case multiple inheritance in `screen.PaintWidget(FrameBuffer, QWidget)` will break, as both + # FrameBuffer and QWidget derive from a different metaclass, and Python cannot figure out which + # class builder to use. + def update(self, + x: Optional[int] = None, + y: Optional[int] = None, + w: Optional[int] = None, + h: Optional[int] = None) -> bool: + raise NotImplementedError() + + +class GraphicLibrary(ABC): + """ + A class interface defining mandatory method a graphical library must implement. + + Currently implemented graphic libraries are `bagl.Bagl` and `nbgl.NBGL`. + """ + + def __init__(self, fb: FrameBuffer, size: Tuple[int, int], model: str): + self._fb = fb + self.SCREEN_WIDTH, self.SCREEN_HEIGHT = size + self.model = model + + @property + def fb(self) -> FrameBuffer: + return self._fb + + @abstractmethod + def refresh(self, data: bytes) -> bool: + pass + + def update_screenshot(self) -> None: + self.fb.update_screenshot() + + def update_public_screenshot(self) -> None: + self.fb.update_public_screenshot() + + def take_screenshot(self) -> Tuple[Tuple[int, int], bytes]: + return self.fb.take_screenshot() class Display(ABC): - def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: - self.notifiers: Dict[int, Server] = {} - self.apdu = server.apdu - self.seph = server.seph - self.model = display.model - self.force_full_ocr = display.force_full_ocr - self.disable_tesseract = display.disable_tesseract - self.rendering = display.rendering + """ + A class interface for managing the graphic display of an application. + + Every display type is composed of two classes: + - A `Display` implementation, which will mostly deal with the graphics + - A `DisplayNotifier`, which will managed the `IOdevice` tied to the running application. + + Currently, there are 3 display management type, stored in following modules: + - `screen.py`, displaying the application infos using Qt + - `screen_text.py`, displaying the application infos using the terminal (through `curses`) + - `headless.py`, displaying nothing, although the application interface can still be reached + through VNC if activated. + """ + + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + self._server_args = server_args + self._display_args = display_args + + @property + def apdu(self) -> Any: # ApduServer: + return self._server_args.apdu + + @property + def seph(self) -> Any: # SeProxyHal: + return self._server_args.seph + + @property + def model(self) -> str: + return self._display_args.model + @property + def rendering(self): + return self._display_args.rendering + + @property @abstractmethod - def display_status(self, data): + def gl(self) -> GraphicLibrary: pass @abstractmethod - def display_raw_status(self, data): + def display_status(self, data: bytes) -> List[TextEvent]: + raise NotImplementedError() + + @abstractmethod + def display_raw_status(self, data: bytes): pass @abstractmethod def screen_update(self) -> bool: pass - def add_notifier(self, klass): - assert klass.s.fileno() not in self.notifiers - self.notifiers[klass.s.fileno()] = klass + def forward_to_app(self, packet: bytes) -> None: + self.seph.to_app(packet) - def remove_notifier(self, fd): - self.notifiers.pop(fd) + def forward_to_apdu_client(self, packet: bytes) -> None: + self.apdu.forward_to_client(packet) - def _init_notifiers(self, args: ServerArgs) -> None: - for klass in args._asdict().values(): - if klass: - self.add_notifier(klass) - def forward_to_app(self, packet): - self.seph.to_app(packet) +class DisplayNotifier(ABC): + """ + A class interface for managing the events between an application display and the `IODevice` + subclasses. - def forward_to_apdu_client(self, packet): - self.apdu.forward_to_client(packet) + Every display type is composed of two classes: + - A `Display` implementation, which will mostly deal with the graphics + - A `DisplayNotifier`, which will managed the `IOdevice` tied to the running application. + + Currently, there are 3 display management type, stored in following modules: + - `screen.py`, displaying the application infos using Qt + - `screen_text.py`, displaying the application infos using the terminal (through `curses`) + - `headless.py`, displaying nothing, although the application interface can still be reached + through VNC if activated. + """ + + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + # TODO: this should be Dict[int, IODevice], but in QtScreen, it is + # a QSocketNotifier, which has a completely different interface + # and is not used in the same way in the mcu/screen.py module. + self.notifiers: Dict[int, Any] = {} + self._server_args = server_args + self._display_args = display_args + self._display: Display + self.__init_notifiers() + + def _set_display_class(self, display_class: type): + self._display = display_class(self._display_args, self._server_args) + + @property + def display(self) -> Display: + return self._display + + def add_notifier(self, device: IODevice): + assert device.fileno not in self.notifiers + self.notifiers[device.fileno] = device + + def remove_notifier(self, fd: int): + self.notifiers.pop(fd) + + def __init_notifiers(self) -> None: + for device in self._server_args._asdict().values(): + if device: + self.add_notifier(device) + + @abstractmethod + def run(self) -> None: + pass diff --git a/speculos/mcu/finger_tcp.py b/speculos/mcu/finger_tcp.py index f0757549..5588254a 100644 --- a/speculos/mcu/finger_tcp.py +++ b/speculos/mcu/finger_tcp.py @@ -11,44 +11,55 @@ import logging import socket +from typing import List +from .display import DisplayNotifier, IODevice -class FakeFingerClient: - def __init__(self, s): - self.s = s + +class FakeFingerClient(IODevice): + def __init__(self, sock: socket.socket): + self.socket = sock self.logger = logging.getLogger("finger") - def _close(self, screen): - screen.remove_notifier(self.s.fileno()) + @property + def file(self): + return self.socket + + def _cleanup(self, screen: DisplayNotifier): + screen.remove_notifier(self.socket.fileno()) self.logger.debug("connection closed with fake button client") - def can_read(self, s, screen): - packet = self.s.recv(100) + def can_read(self, screen: DisplayNotifier): + packet = self.socket.recv(100) if packet == b'': - self._close(screen) + self._cleanup(screen) return - actions = packet.decode("ascii").split(',') - actions = [actions[i * 3:(i + 1) * 3] for i in range(len(actions) // 3)] + _actions: List[str] = packet.decode("ascii").split(',') + actions: List[List[str]] = [_actions[i * 3:(i + 1) * 3] for i in range(len(_actions) // 3)] for action in actions: x = int(action[0]) y = int(action[1]) pressed = int(action[2]) self.logger.debug(f"touch event on ({x},{y}) coordinates, {'pressed' if pressed else 'release'}") - screen.seph.handle_finger(x, y, pressed) + screen.display.seph.handle_finger(x, y, pressed) -class FakeFinger: - def __init__(self, port): - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.s.bind(('0.0.0.0', port)) # lgtm [py/bind-socket-all-network-interfaces] - self.s.listen(5) +class FakeFinger(IODevice): + def __init__(self, port: int): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(('0.0.0.0', port)) # lgtm [py/bind-socket-all-network-interfaces] + self.socket.listen(5) self.logger = logging.getLogger("finger") - def can_read(self, s, screen): - c, addr = self.s.accept() - self.logger.debug(f"new client from {addr}") + @property + def file(self): + return self.socket + + def can_read(self, screen: DisplayNotifier) -> None: + c, addr = self.file.accept() + self.logger.debug("New client from %s", addr) client = FakeFingerClient(c) screen.add_notifier(client) diff --git a/speculos/mcu/headless.py b/speculos/mcu/headless.py index 2aa14643..7a45d53e 100644 --- a/speculos/mcu/headless.py +++ b/speculos/mcu/headless.py @@ -1,72 +1,88 @@ import select -from typing import Optional +from typing import List, Optional +from speculos.observer import TextEvent from . import bagl from . import nbgl -from .display import Display, DisplayArgs, FrameBuffer, Model, MODELS, ServerArgs +from .display import Display, DisplayNotifier, FrameBuffer, GraphicLibrary, MODELS from .readerror import ReadError +from .struct import DisplayArgs, ServerArgs from .vnc import VNC class Headless(Display): def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: super().__init__(display, server) - self._init_notifiers(server) - self.m = HeadlessPaintWidget(self, self.model, server.vnc) + self.m = HeadlessPaintWidget(self.model, server.vnc) + self._gl: GraphicLibrary if display.model != "stax": - self.bagl = bagl.Bagl(self.m, MODELS[self.model].screen_size) + self._gl = bagl.Bagl(self.m, MODELS[self.model].screen_size, self.model) else: - self.nbgl = nbgl.NBGL(self.m, MODELS[self.model].screen_size, display.force_full_ocr, - display.disable_tesseract) + self._gl = nbgl.NBGL(self.m, MODELS[self.model].screen_size, self.model) - def display_status(self, data): - ret = self.bagl.display_status(data) + @property + def gl(self) -> GraphicLibrary: + return self._gl + + def display_status(self, data: bytes) -> List[TextEvent]: + assert isinstance(self.gl, bagl.Bagl) + ret = self.gl.display_status(data) if MODELS[self.model].name == 'blue': self.screen_update() # Actually, this method doesn't work return ret - def display_raw_status(self, data): - self.bagl.display_raw_status(data) + def display_raw_status(self, data: bytes) -> None: + assert isinstance(self.gl, bagl.Bagl) + self.gl.display_raw_status(data) if MODELS[self.model].name == 'blue': self.screen_update() # Actually, this method doesn't work def screen_update(self) -> bool: - return self.bagl.refresh() - - def run(self): - while True: - rlist = self.notifiers.keys() - if not rlist: - break - - rlist, _, _ = select.select(rlist, [], []) - try: - for fd in rlist: - self.notifiers[fd].can_read(fd, self) - - # This exception occur when can_read have no more data available - except ReadError: - break + assert isinstance(self.gl, bagl.Bagl) + return self.gl.refresh() class HeadlessPaintWidget(FrameBuffer): - def __init__(self, parent: Headless, model: Model, vnc: Optional[VNC] = None) -> None: + def __init__(self, model: str, vnc: Optional[VNC] = None): super().__init__(model) self.vnc = vnc - def update(self, x=None, y=None, w=None, h=None): - if self.pixels: + def update(self, + _0: Optional[int] = None, + _1: Optional[int] = None, + _2: Optional[int] = None, + _3: Optional[int] = None) -> bool: + if self.pixels or self.draw_default_color: self._redraw() self.pixels = {} + self.draw_default_color = False return True return False - def _redraw(self): + def _redraw(self) -> None: if self.vnc: - self.vnc.redraw(self.pixels) + self.vnc.redraw(self.pixels, self.default_color) + self.update_screenshot() - self.screenshot_update_pixels() - def update_screenshot(self): - return self.screenshot_update_pixels() +class HeadlessNotifier(DisplayNotifier): + + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + super().__init__(display_args, server_args) + self._set_display_class(Headless) + + def run(self) -> None: + while True: + _rlist = self.notifiers.keys() + if not _rlist: + break + + rlist, _, _ = select.select(_rlist, [], []) + try: + for fd in rlist: + self.notifiers[fd].can_read(self) + + # This exception occur when can_read have no more data available + except ReadError: + break diff --git a/speculos/mcu/nbgl.py b/speculos/mcu/nbgl.py index ae18ef06..8d8cd2f3 100644 --- a/speculos/mcu/nbgl.py +++ b/speculos/mcu/nbgl.py @@ -1,12 +1,25 @@ +import gzip +import logging +import sys from construct import Struct, Int8ul, Int16ul from enum import IntEnum -import gzip +try: + from functools import cache +except ImportError: + # `functools.cache` does not exists on Python3.8 + from functools import lru_cache + cache = lru_cache(maxsize=None) +from typing import Tuple + +from .display import FrameBuffer, GraphicLibrary +# This is a copy - original version is in the SDK (tools/rle_custom.py) +from .rle_custom import RLECustom class NbglColor(IntEnum): - BLACK = 0, - DARK_GRAY = 1, - LIGHT_GRAY = 2, + BLACK = 0 + DARK_GRAY = 1 + LIGHT_GRAY = 2 WHITE = 3 @@ -20,23 +33,27 @@ class NbglColor(IntEnum): ) -class NBGL: - def to_screen_color(color, bpp) -> int: - if bpp == 2: - return color * 0x555555 - if bpp == 1: - return color * 0xFFFFFF - if bpp == 4: - return color * 0x111111 +class NBGL(GraphicLibrary): - def __init__(self, m, size, force_full_ocr, disable_tesseract): - self.m = m - # front screen dimension - self.SCREEN_WIDTH, self.SCREEN_HEIGHT = size - self.force_full_ocr = force_full_ocr - self.disable_tesseract = disable_tesseract + @staticmethod + @cache + def to_screen_color(color: int, bpp: int) -> int: + color_table = { + 1: 0xFFFFFF, + 2: 0x555555, + 4: 0x111111 + } + assert bpp in color_table, f"BPP should be in {color_table.keys()}, but is '{bpp}'" + return color * color_table[bpp] + + def __init__(self, + fb: FrameBuffer, + size: Tuple[int, int], + model: str): + super().__init__(fb, size, model) + self.logger = logging.getLogger("NBGL") - def __assert_area(self, area): + def __assert_area(self, area) -> None: if area.y0 % 4 or area.height % 4: raise AssertionError("X(%d) or height(%d) not 4 aligned " % (area.y0, area.height)) if area.x0 > self.SCREEN_WIDTH or (area.x0+area.width) > self.SCREEN_WIDTH: @@ -44,24 +61,17 @@ def __assert_area(self, area): if area.y0 > self.SCREEN_HEIGHT or (area.y0+area.height) > self.SCREEN_HEIGHT: raise AssertionError("top edge (%d) or bottom edge (%d) out of screen" % (area.y0, (area.y0 + area.height))) - def hal_draw_rect(self, data): + def hal_draw_rect(self, data: bytes) -> None: area = nbgl_area_t.parse(data) - if self.force_full_ocr: - # We need all text shown in black with white background - area.color = 3 - self.__assert_area(area) - for x in range(area.x0, area.x0+area.width): - for y in range(area.y0, area.y0+area.height): - self.m.draw_point(x, y, NBGL.to_screen_color(area.color, 2)) - return + self.fb.draw_rect(area.x0, area.y0, area.width, area.height, NBGL.to_screen_color(area.color, 2)) - def hal_refresh(self, data): + def refresh(self, data: bytes) -> bool: area = nbgl_area_t.parse(data) self.__assert_area(area) - self.m.update(area.x0, area.y0, area.width, area.height) + return self.fb.update(area.x0, area.y0, area.width, area.height) - def hal_draw_line(self, data): + def hal_draw_line(self, data: bytes) -> None: area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) self.__assert_area(area) mask = data[-2] @@ -69,17 +79,21 @@ def hal_draw_line(self, data): back_color = NBGL.to_screen_color(area.color, 2) front_color = NBGL.to_screen_color(color, 2) - for x in range(area.x0, area.x0+area.width): - for y in range(area.y0, area.y0+area.height): - if (mask >> (y-area.y0)) & 0x1: - self.m.draw_point(x, y, front_color) - else: - self.m.draw_point(x, y, back_color) + for y in range(area.y0, area.y0+area.height): + if (mask >> (y-area.y0)) & 0x1: + self.fb.draw_horizontal_line(area.x0, y, area.width, front_color) + else: + self.fb.draw_horizontal_line(area.x0, y, area.width, back_color) + + @staticmethod + @cache def get_color_from_color_map(color, color_map, bpp): # #define GET_COLOR_MAP(__map__,__col__) ((__map__>>(__col__*2))&0x3) return NBGL.to_screen_color((color_map >> (color*2)) & 0x3, bpp) + @staticmethod + @cache def get_4bpp_color_from_color_index(index, front_color, back_color): COLOR_MAPS_4BPP = { # Manually hardcoced color maps @@ -108,6 +122,7 @@ def get_4bpp_color_from_color_index(index, front_color, back_color): mapped_index = COLOR_MAPS_4BPP[(NbglColor(front_color), NbglColor(back_color))][index] return NBGL.to_screen_color(mapped_index, 4) + @staticmethod def nbgl_bpp_to_read_bpp(abpp): if abpp == 0: bpp = 1 @@ -119,25 +134,7 @@ def nbgl_bpp_to_read_bpp(abpp): return 0 return bpp - def hal_draw_image(self, data): - area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) - self.__assert_area(area) - bpp = NBGL.nbgl_bpp_to_read_bpp(area.bpp) - bit_size = (area.width * area.height * bpp) - buffer_size = (bit_size // 8) + ((bit_size % 8) > 0) - buffer = data[nbgl_area_t.sizeof(): nbgl_area_t.sizeof()+buffer_size] - transformation = data[nbgl_area_t.sizeof()+buffer_size] - color_map = data[nbgl_area_t.sizeof()+buffer_size + 1] # front color in case of BPP4 - - if self.force_full_ocr: - # Avoid white on black text - if (bpp == 4) and (color_map == NbglColor.WHITE) and (area.color == NbglColor.BLACK): - area.color = NbglColor.WHITE - color_map = NbglColor.BLACK - elif bpp != 4 and color_map == 3: - area.color = NbglColor.WHITE - color_map = 0 - + def draw_image(self, area, bpp, transformation, buffer, color_map): if transformation == 0: x = area.x0 + area.width - 1 y = area.y0 @@ -155,9 +152,8 @@ def hal_draw_image(self, data): y = area.y0 else: # error - print(transformation) - exit(-2) - pass + self.logger.error("Unknown transformation '%d'", transformation) + sys.exit(-2) if bpp == 1: bit_step = 1 @@ -181,7 +177,7 @@ def hal_draw_image(self, data): else: pixel_color = NBGL.to_screen_color(nib, bpp) - self.m.draw_point(x, y, pixel_color) + self.fb.draw_point(x, y, pixel_color) if transformation == 0: if y < area.y0 + area.height-1: @@ -217,6 +213,17 @@ def hal_draw_image(self, data): # error pass + def hal_draw_image(self, data: bytes): + area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) + self.__assert_area(area) + bpp = NBGL.nbgl_bpp_to_read_bpp(area.bpp) + bit_size = (area.width * area.height * bpp) + buffer_size = (bit_size // 8) + ((bit_size % 8) > 0) + buffer = data[nbgl_area_t.sizeof(): nbgl_area_t.sizeof()+buffer_size] + transformation: int = data[nbgl_area_t.sizeof()+buffer_size] + color_map = data[nbgl_area_t.sizeof()+buffer_size + 1] # front color in case of BPP4 + self.draw_image(area, bpp, transformation, buffer, color_map) + def hal_draw_image_file(self, data): area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) self.__assert_area(area) @@ -245,3 +252,33 @@ def hal_draw_image_file(self, data): data = nbgl_area_t.build(area) + bytes(output_buffer) + b'\0' + data[-1].to_bytes(1, 'big') self.hal_draw_image(data) # decompress + + # ------------------------------------------------------------------------- + def hal_draw_image_rle(self, data): + """ + Draw a bitmap (4BPP or 1BPP) which has been encoded via custom RLE. + Input: + data contains (check sys_nbgl_front_draw_img_rle in src/bolos/nbgl.c) + - area (sizeof(nbgl_area_t)) + - compressed bitmap (buffer_len) + - foreground_color (1 byte) + - nb_skipped_bytes (1 byte) + - character (4 bytes) [added by speculos syscall] + """ + area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) + self.__assert_area(area) + bitmap = data[nbgl_area_t.sizeof():-(1+1+4)] + bpp = NBGL.nbgl_bpp_to_read_bpp(area.bpp) + # We may have to skip initial transparent pixels (bytes, in that case) + nb_skipped_bytes = data[nbgl_area_t.sizeof() + len(bitmap) + 1] + # Uncompress RLE data into buffer + if bpp == 4: + buffer = bytes([0xFF] * nb_skipped_bytes) + else: + buffer = bytes([0x00] * nb_skipped_bytes) + buffer += RLECustom.decode(1, bitmap, bpp) + + # Display the uncompressed image + transformation = 0 # NO_TRANSFORMATION + color_map = data[nbgl_area_t.sizeof() + len(bitmap)] # front color in case of BPP4 + self.draw_image(area, bpp, transformation, buffer, color_map) diff --git a/speculos/mcu/ocr.py b/speculos/mcu/ocr.py index fba01c7b..1f03856e 100644 --- a/speculos/mcu/ocr.py +++ b/speculos/mcu/ocr.py @@ -1,16 +1,21 @@ -from typing import List, Mapping -from dataclasses import dataclass -from PIL import Image -from pytesseract import image_to_data, Output -import base64 import functools -import json -import os import string - -from .automation import TextEvent +from dataclasses import dataclass +from typing import Dict, List, Mapping +from speculos.observer import TextEvent from . import bagl_font +from construct import Struct, Int8ul, Int16ul + +nbgl_area_t = Struct( + "x0" / Int16ul, + "y0" / Int16ul, + "width" / Int16ul, + "height" / Int16ul, + "color" / Int8ul, + "bpp" / Int8ul, +) + MIN_WORD_CONFIDENCE_LVL = 0 # percent NEW_LINE_THRESHOLD = 10 # pixels @@ -21,7 +26,13 @@ Char = str # a single character string (chars are 1-char strings in Python) -__FONT_MAP = {} +@dataclass +class BitMapChar: + char: bagl_font.FontCharacter + bitmap: bytes + + +__FONT_MAP: Dict[int, Mapping[str, BitMapChar]] = {} DISPLAY_CHARS = string.ascii_letters + string.digits + string.punctuation @@ -37,12 +48,6 @@ def wrapper(byte_string: bytes): return wrapper -@dataclass -class BitMapChar: - char: bagl_font.FontCharacter - bitmap: bytes - - def split(bits: BitVector, n: Width) -> List[BitVector]: """ Split a bit array (string of '1' and '0') @@ -81,42 +86,8 @@ def get_char(font: bagl_font.Font, char: str) -> BitMapChar: def display_char(font: bagl_font.Font, char: str) -> None: - char = get_char(font, char) - print("\n".join(split_bytes(char.bitmap, font.bpp * char.char.char_width))) - - -def get_json_font(json_name: str) -> Mapping[Char, BitMapChar]: - # If no json filename was provided, just return - if json_name is None: - return None - - # Add the fonts path (JSON files are in speculos/fonts) - json_name = os.path.join(OCR.fonts_path, json_name) - # Add api level information and file extension - json_name += f"-api-level-{OCR.api_level}.json" - - # Read the JSON file if we found one - font_info = [] - if os.path.exists(json_name): - with open(json_name, "r") as json_file: - font_info = json.load(json_file, strict=False) - font_info = font_info[0] - # Deserialize bitmap - bitmap = base64.b64decode(font_info['bitmap']) - # Build BitMapChar - font_map = {} - for character in font_info['bagl_font_character']: - char = character['char'] - offset = character['bitmap_offset'] - count = character['bitmap_byte_count'] - # Add this entry in font_map - font_map[chr(char)] = BitMapChar( - char, - bytes(bitmap[offset:(offset + count)]), - ) - return font_map - - return None + bm_char = get_char(font, char) + print("\n".join(split_bytes(bm_char.bitmap, font.bpp * bm_char.char.char_width))) def get_font_map(font: bagl_font.Font): @@ -126,11 +97,6 @@ def get_font_map(font: bagl_font.Font): def _get_font_map(font: bagl_font.Font) -> Mapping[Char, BitMapChar]: - # Do we have a JSON file containing all the information we want? - json_font = get_json_font(font.font_name) - if json_font is not None: - return json_font - font_map = {} for ord_char, font_char in zip( range(font.first_char, font.last_char), font.characters @@ -149,100 +115,145 @@ def _get_font_map(font: bagl_font.Font) -> Mapping[Char, BitMapChar]: return font_map -@cache_font -def find_char_from_bitmap(bitmap: BitMap): - """ - Find a character from a bitmap - >>> font = get_font(4) - >>> char = get_char(font, 'c') - >>> find_char_from_bitmap(char.bitmap) - 'c' - """ - all_values = [] - for font in bagl_font.FONTS: - font_map = get_font_map(font) - for character_value, bitmap_struct in font_map.items(): - if bitmap_struct.bitmap == bitmap: - all_values.append(character_value) - if all_values: - char = max([x for x in all_values]) - if char == "\x80": - char = " " - return char - - class OCR: - api_level = 0 - fonts_path = "" + # Maximum space for a letter to be considered part of the same word + MAX_BLANK_SPACE_NANO = 12 + MAX_BLANK_SPACE_STAX = 24 - def __init__(self, fonts_path=None, api_level=None): + def __init__(self, model: str): self.events: List[TextEvent] = [] - # Save fonts path & the API_LEVEL in a class variable - if fonts_path is not None: - OCR.fonts_path = fonts_path - if api_level is not None: - OCR.api_level = int(api_level) - - def analyze_bitmap(self, data: bytes): - if data[0] != 0: - return - - x = int.from_bytes(data[1:3], byteorder="big", signed=True) - y = int.from_bytes(data[3:5], byteorder="big", signed=True) - w = int.from_bytes(data[5:7], byteorder="big", signed=True) - h = int.from_bytes(data[7:9], byteorder="big", signed=True) - bpp = int.from_bytes(data[9:10], byteorder="big") - color_size = 4 * (1 << bpp) - bitmap = data[10+color_size:] - - # h may no reflect the real char height: use number of lines displayed - h = (len(bitmap) * 8) // w - if (len(bitmap) * 8) % w: - h += 1 - - # Space is now encoded as an empty character (no 'space' wasted :) - if len(bitmap) == 0: - char = ' ' + # Store the model of the device + self.model = model + # Maximum space for a letter to be considered part of the same word + if model == "stax": + self.max_blank_space = OCR.MAX_BLANK_SPACE_STAX else: - char = find_char_from_bitmap(bitmap) + self.max_blank_space = OCR.MAX_BLANK_SPACE_NANO + + @staticmethod + def find_char_from_bitmap(bitmap: BitMap) -> str: + """ + Find a character from a bitmap + >>> font = get_font(4) + >>> char = get_char(font, 'c') + >>> find_char_from_bitmap(char.bitmap) + 'c' + """ + all_values = [] + for font in bagl_font.FONTS: + font_map = get_font_map(font) + for character_value, bitmap_struct in font_map.items(): + if bitmap_struct.bitmap.startswith(bitmap): + # sometimes (but not always) the bitmap being passed is shortened + # by one '\x00' byte, not matching the exact bitmap + # provided in the font. Hence the 'residual' computation + residual_bytes: bytes = bitmap_struct.bitmap[len(bitmap):] + if all(b == 0 for b in residual_bytes): + all_values.append(character_value) + if all_values: + char = max([x for x in all_values]) + if char == "\x80": + char = " " + return char + return "" + + def find_bitmap(self, x: int, y: int, w: int, h: int, bitmap: bytes) -> None: + char = self.find_char_from_bitmap(bitmap) if char: - if self.events and y <= (self.events[-1].y + self.events[-1].h): - # Add this character to previous event + if self.events and y <= self.events[-1].y: self.events[-1].text += char - # Update w for all chars in self.events[-1] - x2 = x + w - 1 - self.events[-1].w = x2 - self.events[-1].x + 1 - # Update y & h, if needed, for all chars in self.events[-1] - y1 = y - if y1 > self.events[-1].y: - # Keep the lowest Y in Y1 - y1 = self.events[-1].y - y2 = y + h - 1 - if y2 < (self.events[-1].y + self.events[-1].h): - # Keep the highest Y in Y2 - y2 = self.events[-1].y + self.events[-1].h - 1 - self.events[-1].y = y1 - self.events[-1].h = y2 - y1 + 1 else: - # create a new TextEvent if there are no events yet or if there is a new line + # create a new TextEvent if there are no events yet + # or if there is a new line self.events.append(TextEvent(char, x, y, w, h)) - def analyze_image(self, screen_size: (int, int), data: bytes): - image = Image.frombytes("RGB", screen_size, data) - data = image_to_data(image, output_type=Output.DICT) - new_text_has_been_added = False - for item in range(len(data["text"])): - if (data["conf"][item] > MIN_WORD_CONFIDENCE_LVL): - if new_text_has_been_added and self.events and \ - data["top"][item] <= self.events[-1].y + NEW_LINE_THRESHOLD: - self.events[-1].text += " "+data["text"][item] - else: - x = data["left"][item] - y = data["top"][item] - w, h = screen_size - self.events.append(TextEvent(data['text'][item], x, y, w, h)) - new_text_has_been_added = True + def store_char_in_last_event(self, x: int, y: int, w: int, h: int, char: str) -> None: + """ + Add current character to last event + """ + self.events[-1].text += char + # Update w for all chars in self.events[-1] + x2 = x + w - 1 + self.events[-1].w = x2 - self.events[-1].x + 1 + # Update y & h, if needed, for all chars in self.events[-1] + y1 = y + if y1 > self.events[-1].y: + # Keep the lowest Y in Y1 + y1 = self.events[-1].y + y2 = y + h - 1 + if y2 < (self.events[-1].y + self.events[-1].h): + # Keep the highest Y in Y2 + y2 = self.events[-1].y + self.events[-1].h - 1 + self.events[-1].y = y1 + self.events[-1].h = y2 - y1 + 1 + + def add_character(self, x: int, y: int, w: int, h: int, char: str) -> None: + """ + Add the provided character to previous event or create a new one + """ + # Compute difference with X coord from previous event + # if x_diff > self.max_blank_space the char is not on same sentence + if self.events: + x_diff = x - (self.events[-1].x + self.events[-1].w) + if x_diff < 0: + x_diff = -x_diff + # Try to find if that char can be added to previous event + if y < (self.events[-1].y + self.events[-1].h) \ + and x_diff < self.max_blank_space: + # Add this character to previous event + self.store_char_in_last_event(x, y, w, h, char) + return + + # create a new TextEvent if there are no events yet or if there is a new line + self.events.append(TextEvent(char, x, y, w, h)) + + def analyze_bitmap(self, data: bytes) -> None: + """ + data contain information about the latest displayed bitmap. + Since unified SDK, speculos added the displayed character to data. + For older SKD versions, legacy behaviour is used: parsing internal + fonts to find a matching bitmap. + """ + if self.model == "stax": + # Can be called via SephTag.NBGL_DRAW_IMAGE or SephTag.NBGL_DRAW_IMAGE_RLE + # In both cases, data contains: + # - area (sizeof(nbgl_area_t)) + # - compressed bitmap (buffer_len) + # - 2 bytes of different meaning depending on SephTag + # - 4 bytes with unicode code point of displayed character + area = nbgl_area_t.parse(data[0:nbgl_area_t.sizeof()]) + x, y, w, h = area.x0, area.y0, area.width, area.height + character = int.from_bytes(data[-4:], byteorder="little", signed=False) + bitmap = data[nbgl_area_t.sizeof():-(2+4)] + else: + if data[0] != 0: + return + + x = int.from_bytes(data[1:3], byteorder="big", signed=True) + y = int.from_bytes(data[3:5], byteorder="big", signed=True) + w = int.from_bytes(data[5:7], byteorder="big", signed=True) + h = int.from_bytes(data[7:9], byteorder="big", signed=True) + bpp = int.from_bytes(data[9:10], byteorder="big") + character = int.from_bytes(data[10:14], byteorder="big", signed=False) + color_size = 4 * (1 << bpp) + bitmap = data[14+color_size:] + + # h may no reflect the real height: use number of lines displayed + h = (len(bitmap) * 8) // w + if (len(bitmap) * 8) % w: + h += 1 + + # Space is now encoded as an empty character (no 'space' wasted :) + if len(bitmap) == 0: + character = 32 + + # If data contains a character, don't bother doing OCR, just store it! + if character != 0: + char = chr(character) + self.add_character(x, y, w, h, char) + else: + self.find_bitmap(x, y, w, h, bitmap) def get_events(self) -> List[TextEvent]: events = self.events.copy() diff --git a/speculos/mcu/rle_custom.py b/speculos/mcu/rle_custom.py new file mode 100755 index 00000000..f98892d6 --- /dev/null +++ b/speculos/mcu/rle_custom.py @@ -0,0 +1,1216 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# ----------------------------------------------------------------------------- +""" +This module contain tools to test different custom RLE coding/decoding. +""" +# ----------------------------------------------------------------------------- +import argparse +import sys + + +# ----------------------------------------------------------------------------- +# Regular RLE encoding +# ----------------------------------------------------------------------------- +class RLECustomBase: + """ + Class handling regular RLE encoding (1st pass) + """ + CMD_FILL = 0 + CMD_COPY = 1 + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__() + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + self.encoded = None + + # ------------------------------------------------------------------------- + def __enter__(self): + """ + Return an instance to this object. + """ + return self + + # ------------------------------------------------------------------------- + def __exit__(self, exec_type, exec_value, traceback): + """ + Do all the necessary cleanup. + """ + + # ------------------------------------------------------------------------- + @staticmethod + def remove_duplicates(pairs): + """ + Check if there are some duplicated pairs (same values) and merge them. + """ + index = len(pairs) - 1 + while index >= 1: + repeat1, value1 = pairs[index-1] + repeat2, value2 = pairs[index] + # Do we have identical consecutives values? + if value1 == value2: + repeat1 += repeat2 + # Merge them and remove last entry + pairs[index-1] = (repeat1, value1) + pairs.pop(index) + index -= 1 + + return pairs + + # ------------------------------------------------------------------------- + @staticmethod + def bpp4_to_values(data): + """ + Expand each bytes of data into 2 quartets. + Return an array of values (from 0x00 to 0x0F) + """ + output = [] + for byte in data: + lsb_bpp4 = byte & 0x0F + byte >>= 4 + output.append(byte & 0x0F) + output.append(lsb_bpp4) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def values_to_bpp4(data): + """ + Takes values (assumed from 0x00 to 0x0F) in data and returns an array + of bytes containing quartets with values concatenated. + """ + output = bytes() + remaining_values = len(data) + index = 0 + while remaining_values > 1: + byte = data[index] & 0x0F + index += 1 + byte <<= 4 + byte |= data[index] & 0x0F + index += 1 + remaining_values -= 2 + output += bytes([byte]) + + # Is there a remaining quartet ? + if remaining_values != 0: + byte = data[index] & 0x0F + byte <<= 4 # Store it in the MSB + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def bpp1_to_values(data): + """ + Expand each bytes of data into 8 bits, each stored in a byte + Return an array of values (containing bytes values 0 or 1) + """ + output = [] + for byte in data: + # first pixel is in bit 10000000 + for shift in range(7, -1, -1): + pixel = byte >> shift + pixel &= 1 + output.append(pixel) + + assert len(output) == (len(data)*8) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def values_to_bpp1(data): + """ + Takes values (bytes containing 0 or 1) in data and returns an array + of bytes containing bits concatenated. + (first pixel is bit 10000000 of first byte) + """ + output = bytes() + remaining_values = len(data) + index = 0 + bits = 7 + byte = 0 + while remaining_values > 0: + pixel = data[index] & 1 + index += 1 + byte |= pixel << bits + bits -= 1 + if bits < 0: + # We read 8 pixels: store them and get ready for next ones + output += bytes([byte]) + bits = 7 + byte = 0 + remaining_values -= 1 + + # Is there some remaining pixels stored? + if bits != 7: + output += bytes([byte]) + + nb_bytes = len(data)/8 + if len(data) % 8: + nb_bytes += 1 + assert len(output) == nb_bytes + + return output + + # ------------------------------------------------------------------------- + def bpp_to_values(self, data): + """ + Expand each bytes of data, containing pixels (4BPP or 1BPP) + Return an array of values (0x00 to 0x0F or 0x00 to 0x01) + """ + if self.bpp == 4: + return self.bpp4_to_values(data) + + return self.bpp1_to_values(data) + + # ------------------------------------------------------------------------- + def values_to_bpp(self, data): + """ + Takes values (0x00 to 0x0F or 0x00 to 0x01) in data and returns an + array of bytes containing pixels concatenated. + """ + if self.bpp == 4: + return self.values_to_bpp4(data) + + return self.values_to_bpp1(data) + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs, max_count=128): + """ + Encode tuples containing (repeat, val) into packed values. + (empty method intended to be overwritten) + """ + if max_count: + pass + + return pairs + + # ------------------------------------------------------------------------- + @staticmethod + def decode_pass2(data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + (empty method intended to be overwritten) + """ + return data + + # ------------------------------------------------------------------------- + def encode_pass1(self, data): + """ + Encode array of values using 'normal' RLE. + Return an array of tuples containing (repeat, val) + """ + output = [] + previous_value = -1 + count = 0 + for value in data: + # Same value than before? + if value == previous_value: + count += 1 + else: + # Store previous result + if count: + pair = (count, previous_value) + output.append(pair) + # Start a new count + previous_value = value + count = 1 + + # Store previous result + if count: + pair = (count, previous_value) + output.append(pair) + + if self.verbose: + sys.stdout.write(f"Nb values: {len(output)}\n") + for repeat, value in output: + sys.stdout.write(f"{repeat:3d} x 0x{value:02X}\n") + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def decode_pass1(data): + """ + Decode array of tuples containing (repeat, val). + Return an array of values. + """ + output = [] + for repeat, value in data: + for _ in range(repeat): + output.append(value) + + return output + + # ------------------------------------------------------------------------- + def encode(self, data): + """ + Input: array of packed pixels + - convert to an array of values + - convert to tuples of (repeat, value) + - encode using custom RLE + Output: array of compressed bytes + """ + values = self.bpp_to_values(data) + pairs = self.encode_pass1(values) + + # Second pass + self.encoded = self.encode_pass2(pairs) + + # Decode to check everything is fine + decompressed = self.decode(self.encoded) + if decompressed != data: + # This will never happen in prod environment, only when testing + sys.stdout.write("Encoding/Decoding is WRONG!!!\n") + sys.exit(111) + + return self.encoded + + # ------------------------------------------------------------------------- + def decode(self, data): + """ + Input: array of compressed bytes + - decode to pairs using custom RLE + - convert the tuples of (repeat, value) to values + - convert to an array of packed pixels + Output: array of packed pixels + """ + pairs = self.decode_pass2(data) + values = self.decode_pass1(pairs) + + decoded = self.values_to_bpp(values) + + return decoded + + # ------------------------------------------------------------------------- + @staticmethod + def get_encoded_size(data): + """ + Return the encoded size. + (to be overwritten if something more complex than len() need to be done) + """ + return len(data) + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte +# ----------------------------------------------------------------------------- +class RLECustom1 (RLECustomBase): + """ + Class handling custom RLE encoding (2nd pass) + The generated bytes will contain: + * for 1 BPP: RRRRRRRV + - RRRRRRR: repeat count - 1 => allow to store 1 to 256 repeat counts + - V: value of the 1BPP pixel (0 or 1) + * for 4 BPP: RRRRVVVV + - RRRR: repeat count - 1 => allow to store 1 to 16 repeat counts + - VVVV: value of the 4BPP pixel + """ + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs, max_count=16): + """ + Encode tuples containing (repeat, val) into packed values. + The generated bytes will contain: + RRRRVVVV + - RRRR: repeat count - 1 => allow to store 1 to 16 repeat counts + - VVVV: value of the 4BPP pixel + """ + output = bytes() + for repeat, value in pairs: + # We can't store more than a repeat count of max_count + while repeat > max_count: + count = max_count - 1 + byte = count << 4 + byte |= value & 0x0F + output += bytes([byte]) + repeat -= max_count + if repeat: + count = repeat - 1 + byte = count << 4 + byte |= value & 0x0F + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The bytes provided contains: + RRRRVVVV + - RRRR: repeat count - 1 => allow to store 1 to 16 repeat counts + - VVVV: value of the 4BPP pixel + """ + pairs = [] + for byte in data: + value = byte & 0x0F + byte >>= 4 + byte &= 0x0F + repeat = 1 + byte + pairs.append((repeat, value)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte + +# - white handling +# ----------------------------------------------------------------------------- +class RLECustom2 (RLECustom1): + """ + Class handling custom RLE encoding (2nd pass) + The generated bytes will contain: + 1XXXXXXX + 0XXXXXXX + With: + * 1RRRRRRR + - RRRRRRRR is repeat count - 1 of White (0xF) quartets + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs, max_count=128): + """ + Encode tuples containing (repeat, val) into packed values. + The generated bytes will contain: + 1XXXXXXX + 0XXXXXXX + With: + * 1RRRRRRR + - RRRRRRRR is repeat count - 1 of White (0xF) quartets (max=127+1) + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + output = bytes() + for repeat, value in pairs: + # Handle white + if value == 0x0F: + # We can't store more than a repeat count of 128 + while repeat > max_count: + byte = 0x80 + byte |= max_count - 1 + output += bytes([byte]) + repeat -= max_count + if repeat: + byte = 0x80 + byte |= repeat - 1 + output += bytes([byte]) + else: + # We can't store more than a repeat count of 8 + while repeat > 8: + count = 8 - 1 + byte = count << 4 + byte |= value & 0x0F + output += bytes([byte]) + repeat -= 8 + if repeat: + count = repeat - 1 + byte = count << 4 + byte |= value & 0x0F + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The bytes provided contains: + 1XXXXXXX + 0XXXXXXX + With: + * 1RRRRRRR + - RRRRRRRR is repeat count - 1 of White (0xF) quartets + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + pairs = [] + for byte in data: + # Is it a big duplication of whites ? + if byte & 0x80: + byte &= 0x7F + repeat = 1 + byte + value = 0x0F + else: + value = byte & 0x0F + byte >>= 4 + byte &= 0x07 + repeat = 1 + byte + pairs.append((repeat, value)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte + +# - white handling +# - copy range of bytes +# ----------------------------------------------------------------------------- +class RLECustom3 (RLECustom2): + """ + Class handling custom RLE encoding (2nd pass) + The generated bytes will contain: + 11RRRRRR + 10RRVVVV WWWWXXXX YYYYZZZZ QQQQ0000 + 0RRRVVVV + With: + * 11RRRRRR + - RRRRRRR is repeat count - 1 of White (0xF) quartets (max=63+1) + * 10RRVVVV WWWWXXXX YYYYZZZZ QQQQ0000 + - RR is repeat count - 3 of quartets (max=3+3 => 6 quartets) + - VVVV: value of 1st 4BPP pixel + - WWWW: value of 2nd 4BPP pixel + - XXXX: value of 3rd 4BPP pixel + - YYYY: value of 4th 4BPP pixel + - ZZZZ: value of 5th 4BPP pixel + - QQQQ: value of 6th 4BPP pixel + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs, max_count=64): + """ + Encode tuples containing (repeat, val) into packed values. + The generated bytes will contain: + 11RRRRRR + 10RRVVVV WWWWXXXX YYYYZZZZ + 0RRRVVVV + With: + * 11RRRRRR + - RRRRRRR is repeat count - 1 of White (0xF) quartets (max=63+1) + * 10RRVVVV WWWWXXXX YYYYZZZZ QQQQ0000 + - RR is repeat count - 3 of quartets (max=3+3 => 6 quartets) + - VVVV: value of 1st 4BPP pixel + - WWWW: value of 2nd 4BPP pixel + - XXXX: value of 3rd 4BPP pixel + - YYYY: value of 4th 4BPP pixel + - ZZZZ: value of 5th 4BPP pixel + - QQQQ: value of 6th 4BPP pixel + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + # First, generate data in 10RRRRRR/0RRRVVVV format + single_output = RLECustom2.encode_pass2(pairs, max_count) + + # Now, parse array to find consecutives singles (0000VVVV) + output = bytes() + index = 0 + while index < len(single_output): + # Do we have some 'singles'? + byte = single_output[index] + if (byte & 0xF0) != 0x00: + # No, just store it + if byte & 0x80: + byte |= 0x40 # 11RRRRRR + output += bytes([byte]) + index += 1 + else: + # Check how many 'singles' we have + count = 1 + while ((index+count) < len(single_output)) and \ + (single_output[index+count] & 0xF0) == 0x00: + count += 1 + # No more than 6 singles + if count > 6: + # Special case: if count = 8 then do 5+3 + if count == 8: + count = 5 # to allow storing next 3 singles!! + else: + count = 6 + # Do we have at least 3 singles? + if count >= 3: + # Store 10RRVVVV WWWWXXXX YYYYZZZZ QQQQ0000 + # 3 singles => 1 + 1 bytes + # 4 singles => 1 + 2 bytes (1 quartet unused) + # 5 singles => 1 + 2 bytes + # 6 singles => 1 + 3 bytes (1 quartet unused) + msb_byte = count - 3 + msb_byte <<= 4 + msb_byte |= 0x80 + byte |= msb_byte + # Store first byte + output += bytes([byte]) + index += 1 + count -= 1 + while count > 0: + byte = single_output[index] # No need to mask + index += 1 + count -= 1 + byte <<= 4 + # Do we have an other quartet? + if count > 0: + byte |= single_output[index] # No need to mask + index += 1 + count -= 1 + # Store the quartet(s) + output += bytes([byte]) + else: + # No, just store that single + output += bytes([byte]) + index += 1 + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The bytes provided contains: + 11RRRRRR + 10RRVVVV WWWWXXXX YYYYZZZZ + 0RRRVVVV + With: + * 11RRRRRR + - RRRRRRR is repeat count - 1 of White (0xF) quartets (max=63+1) + * 10RRVVVV WWWWXXXX YYYYZZZZ QQQQ0000 + - RR is repeat count - 3 of quartets (max=3+3 => 6 quartets) + - VVVV: value of 1st 4BPP pixel + - WWWW: value of 2nd 4BPP pixel + - XXXX: value of 3rd 4BPP pixel + - YYYY: value of 4th 4BPP pixel + - ZZZZ: value of 5th 4BPP pixel + - QQQQ: value of 6th 4BPP pixel + * 0RRRVVVV + - RRR: repeat count - 1 => allow to store 1 to 8 repeat counts + - VVVV: value of the 4BPP pixel + """ + pairs = [] + index = 0 + while index < len(data): + byte = data[index] + index += 1 + # Is it a big duplication of whites or singles? + if byte & 0x80: + # Is it a big duplication of whites? + if byte & 0x40: + # 11RRRRRR + byte &= 0x3F + repeat = 1 + byte + value = 0x0F + # We need to decode singles + else: + # 10RRVVVV WWWWXXXX YYYYZZZZ + count = (byte & 0x30) + count >>= 4 + count += 3 + value = byte & 0x0F + pairs.append((1, value)) + count -= 1 + while count > 0 and index < len(data): + byte = data[index] + index += 1 + value = byte >> 4 + value &= 0x0F + pairs.append((1, value)) + count -= 1 + if count > 0: + value = byte & 0x0F + pairs.append((1, value)) + count -= 1 + continue + else: + # 0RRRVVVV + value = byte & 0x0F + byte >>= 4 + byte &= 0x07 + repeat = 1 + byte + + pairs.append((repeat, value)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte +# ----------------------------------------------------------------------------- +class RLECustom4 (RLECustomBase): + """ + Class handling custom RLE encoding (2nd pass) + The generated bytes will contain: + RRRRRRRV + - RRRRRRRRR: repeat count - 1 => allow to store 1 to 128 repeat counts + - V: value of the 1BPP pixel + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs, max_count=128): + """ + Encode tuples containing (repeat, val) into packed values. + The generated bytes will contain: + RRRRRRRV + - RRRRRRRRR: repeat count - 1 => allow to store 1 to 128 repeat counts + - V: value of the 1BPP pixel + """ + output = bytes() + for repeat, value in pairs: + # We can't store more than a repeat count of max_count + while repeat > max_count: + count = max_count - 1 + byte = count << 1 + byte |= value & 1 + output += bytes([byte]) + repeat -= max_count + if repeat: + count = repeat - 1 + byte = count << 1 + byte |= value & 1 + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The bytes provided contains: + RRRRRRRV + - RRRRRRRRR: repeat count - 1 => allow to store 1 to 128 repeat counts + - V: value of the 1BPP pixel + """ + pairs = [] + for byte in data: + value = byte & 1 + byte >>= 1 + byte &= 0x7F + repeat = 1 + byte + pairs.append((repeat, value)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte +# ----------------------------------------------------------------------------- +class RLECustomN (RLECustomBase): + """ + Class handling custom RLE encoding (2nd pass) + The generated bytes will contain triplets with: + - Command (Fill or Copy) + - Repeat count + - Value: value(s) to be Filled/Copied + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + self.commands = [] + self.repeat = [] + self.values = [] + + # ------------------------------------------------------------------------- + def encode_pass2(self, pairs, max_count=64): + """ + Encode tuples containing (repeat, val) into packed values. + The generated packed values will contain: + - Command (Fill or Copy) + - Repeat count + - Value: value(s) to be Filled/Copied + """ + infos = [] + index = 0 + if self.bpp == 1: + # threshold = 7 + threshold = 2 + else: + threshold = 3 + + while index < len(pairs): + repeat, value = pairs[index] + index += 1 + # We can't store more than a repeat count of max_count + while repeat >= max_count: + pixels = [value] + info = (self.CMD_FILL, max_count, pixels) + infos.append(info) + repeat -= max_count + + # Is it still interesting to use FILL commands? + if repeat >= threshold: + pixels = [value] + info = (self.CMD_FILL, repeat, pixels) + infos.append(info) + # No, use COPY commands + elif repeat: + pixels = [] + for _ in range(repeat): + pixels.append(value) + + # Can we merge next pixels into this COPY command? + while index < len(pairs) and pairs[index][0] < threshold: + repeat2, value2 = pairs[index] + index += 1 + + while repeat2 > 0: + pixels.append(value2) + repeat2 -= 1 + + if len(pixels) == max_count: + info = (self.CMD_COPY, len(pixels), pixels) + infos.append(info) + pixels = [] + + # Store remaining pixels + if len(pixels) > 0: + info = (self.CMD_COPY, len(pixels), pixels) + infos.append(info) + + if self.verbose: + sys.stdout.write(f"Nb values: {len(infos)}\n") + for command, repeat, pixels in infos: + if command == self.CMD_FILL: + value = pixels[0] + sys.stdout.write(f"FILL {repeat:3d} x 0x{value:02X}\n") + else: + sys.stdout.write(f"COPY {repeat:3d} x {pixels}\n") + + return infos + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The provided packed values will contain: + - Command (Fill or Copy) + - Repeat count + - Value: value(s) to be Filled/Copied + """ + pairs = [] + for command, repeat, pixels in data: + if command == self.CMD_FILL: + value = pixels[0] + pairs.append((repeat, value)) + else: + # Store pixel by pixel => duplicates will be merged later + for value in pixels: + pairs.append((1, value)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + # ------------------------------------------------------------------------- + def get_encoded_size(self, data): + """ + Return the encoded size. + """ + # Size needed to store the commands + cmd_size = len(data) // 8 + if len(data) % 8: + cmd_size += 1 + + # Size needed to store the repeat count + count_size = len(data) // 2 + if len(data) % 2: + count_size += 1 + + # Compute size needed by pixels + total_pixels = 0 + bits_for_pixels = 0 + for command, repeat, pixels in data: + total_pixels += repeat + if command == self.CMD_FILL: + bits_for_pixels += self.bpp + else: + bits_for_pixels += self.bpp * repeat + assert repeat == len(pixels) + + # Size needed by pixels + pixels_size = bits_for_pixels // 8 + if bits_for_pixels % 8: + pixels_size += 1 + + sys.stdout.write(f"Nb pixels: {total_pixels}\n") + sys.stdout.write(f"sizes: cmd={cmd_size}, count={count_size}" + f", data={pixels_size}\n") + + return cmd_size + count_size + pixels_size + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte +# ----------------------------------------------------------------------------- +class RLECustomA (RLECustomBase): + """ + Class handling custom RLE encoding (2nd pass) for 1 BPP only + The generated bytes will contain Alternances counts ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + if self.bpp != 1: + sys.stdout.write("The class RLECustomA is for 1 BPP data only!\n") + sys.exit(1) + + # ------------------------------------------------------------------------- + def encode_pass2(self, pairs, max_count=15): + """ + Encode Alternance counts between pixels (nb of 0, then nb of 1, etc) + The generated packed values will contain ZZZZOOOO ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + # First step: store alternances, and split if count > 15 + next_pixel = 0 + alternances = [] + for repeat, value in pairs: + # Store a count of 0 pixel if next pixel is not the one expected + if value != next_pixel: + alternances.append(0) + next_pixel ^= 1 + + while repeat > max_count: + # Store 15 pixels of value next_pixel + alternances.append(max_count) + repeat -= max_count + # Store 0 pixel of alternate value + alternances.append(0) + + if repeat: + alternances.append(repeat) + next_pixel ^= 1 + + if False and self.verbose: + sys.stdout.write(f"Nb values: {len(alternances)}\n") + next_pixel = 0 + for repeat in alternances: + sys.stdout.write(f"{repeat:2d} x {next_pixel}\n") + next_pixel ^= 1 + + # Now read all those values and store them into quartets + output = bytes() + index = 0 + + while index < len(alternances): + zeros = alternances[index] + index += 1 + if index < len(alternances): + ones = alternances[index] + index += 1 + else: + ones = 0 + + byte = (zeros << 4) | ones + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The provided packed values will contain ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + pairs = [] + for byte in data: + ones = byte & 0x0F + byte >>= 4 + zeros = byte & 0x0F + if zeros: + pairs.append((zeros, 0)) + if ones: + pairs.append((ones, 1)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Custom RLE encoding: pack repeat count & value into 1 byte +# ----------------------------------------------------------------------------- +class RLECustomB (RLECustomBase): + """ + Class handling custom RLE encoding (2nd pass) for 1 BPP only + The generated bytes will contain Alternances counts ZZZZZZZZ OOOOOOOO with + - ZZZZZZZZ: number of consecutives 0, from 0 to 255 + - OOOOOOOO: number of consecutives 1, from 0 to 255 + TODO: check the same with 6 bits for repeat count + ZZZZZZOO OOOOZZZZ ZZOOOOOO + - ZZZZZZ: number of consecutives 0, from 0 to 63 + - OOOOOO: number of consecutives 1, from 0 to 63 + """ + + # ------------------------------------------------------------------------- + def __init__(self, bpp=1, verbose=False): + super().__init__(bpp, verbose) + + # Store parameters: + self.bpp = bpp + self.verbose = verbose + + if self.bpp != 1: + sys.stdout.write("The class RLECustomB is for 1 BPP data only!\n") + sys.exit(1) + + # ------------------------------------------------------------------------- + def encode_pass2(self, pairs, max_count=255): + """ + Encode Alternance counts between pixels (nb of 0, then nb of 1, etc) + The generated packed values will contain ZZZZZZZZ OOOOOOOO with + - ZZZZZZZZ: number of consecutives 0, from 0 to 255 + - OOOOOOOO: number of consecutives 1, from 0 to 255 + """ + # First step: store alternances, and split if count > 255 + next_pixel = 0 + alternances = [] + for repeat, value in pairs: + # Store a count of 0 pixel if next pixel is not the one expected + if value != next_pixel: + alternances.append(0) + next_pixel ^= 1 + + while repeat > max_count: + # Store 255 pixels of value next_pixel + alternances.append(max_count) + repeat -= max_count + # Store 0 pixel of alternate value + alternances.append(0) + + if repeat: + alternances.append(repeat) + next_pixel ^= 1 + + if self.verbose: + sys.stdout.write(f"Nb values: {len(alternances)}\n") + + if False and self.verbose: + sys.stdout.write(f"Nb values: {len(alternances)}\n") + next_pixel = 0 + for repeat in alternances: + sys.stdout.write(f"{repeat:2d} x {next_pixel}\n") + next_pixel ^= 1 + + # Now read all those values and store them into bytes + output = bytes() + index = 0 + + while index < len(alternances): + zeros = alternances[index] + index += 1 + if index < len(alternances): + ones = alternances[index] + index += 1 + else: + ones = 0 + + output += bytes([zeros]) + output += bytes([ones]) + + return output + + # ------------------------------------------------------------------------- + def decode_pass2(self, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The generated bytes will contain Alternances counts ZZZZZZZZ OOOOOOOO with + - ZZZZZZZZ: number of consecutives 0, from 0 to 255 + - OOOOOOOO: number of consecutives 1, from 0 to 255 + """ + pairs = [] + for index, repeat in enumerate(data): + if index & 1: + pairs.append((repeat, 1)) + else: + pairs.append((repeat, 0)) + + # There was a limitation on repeat count => remove duplicates + pairs = self.remove_duplicates(pairs) + + return pairs + + +# ----------------------------------------------------------------------------- +# Entry point for easy RLE encoding/decoding +# ----------------------------------------------------------------------------- +class RLECustom: + """ + Class handling custom RLE encoding + """ + # ------------------------------------------------------------------------- + @classmethod + def encode(cls, packed_pixels, bpp, verbose=False): + """ + Try different encoding algorithms to compress 4 or 1BPP pixels. + Input: + - array of packed pixels + - number of bpp (1 or 4) + Output: Tuple containing: + - method used to encode data + - bytes array containing encoded data + """ + # Default values: no encoding + method = 0 + compressed = packed_pixels + # Try different methods depending on BPP + if bpp == 4: + # For now, just compress 4 BPP bitmap with RLECustom3 + rle = RLECustom3(bpp, verbose) + compressed = rle.encode(packed_pixels) + method = 1 + + elif bpp == 1: + # For now, just compress 1 BPP bitmap with RLECustomA + rle = RLECustomA(bpp, verbose) + compressed = rle.encode(packed_pixels) + method = 1 + + return method, compressed + + # ------------------------------------------------------------------------- + @classmethod + def decode(cls, method, encoded_data, bpp, verbose=False): + """ + Decompress previously encoded data using the provided method. + Input: array of compressed bytes + Output: array of packed pixels + """ + # Is data really encoded? + decoded = encoded_data + if method != 1: + pass + elif bpp == 4: + # Just one method for the moment + rle = RLECustom3(bpp, verbose) + decoded = rle.decode(encoded_data) + elif bpp == 1: + # Just one method for the moment + rle = RLECustomA(bpp, verbose) + decoded = rle.decode(encoded_data) + + return decoded + + +# ----------------------------------------------------------------------------- +# Program entry point: +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + + # ------------------------------------------------------------------------- + def main(args): + """ + Main method. + """ + # ascii 0x0040 (88 bytes) + data = bytes([ + 0x00, 0x7F, 0xE0, 0x00, 0x1F, 0xFF, 0x00, 0x07, + 0xC0, 0x38, 0x00, 0xF0, 0x01, 0x80, 0x1E, 0x00, + 0x18, 0x03, 0x80, 0x01, 0x80, 0x30, 0x00, 0x38, + 0x67, 0x1F, 0xFF, 0x86, 0x61, 0xFF, 0xF0, 0x6E, + 0x0C, 0x03, 0x07, 0xC1, 0xC0, 0x38, 0x3C, 0x18, + 0x01, 0x83, 0xC1, 0x80, 0x18, 0x3C, 0x18, 0x01, + 0x83, 0xC1, 0x80, 0x18, 0x3E, 0x1C, 0x03, 0x87, + 0x60, 0xF0, 0xF0, 0x67, 0x07, 0xFE, 0x0E, 0x30, + 0x1F, 0x80, 0xC3, 0x80, 0x00, 0x1C, 0x1E, 0x00, + 0x07, 0x80, 0xF0, 0x00, 0xF0, 0x07, 0xC0, 0x3E, + 0x00, 0x1F, 0xFF, 0x80, 0x00, 0x7F, 0xE0, 0x00, + ]) + with RLECustomA(args.bpp, args.verbose) as rle: + compressed = rle.encode(data) + encoded_size = rle.get_encoded_size(compressed) + sys.stdout.write(f"Encoded size: {encoded_size} bytes " + f"(instead of {len(data)})\n") + # No need to check if decoding is fine, already done when encoding + + return 0 + + # ------------------------------------------------------------------------- + # Parse arguments: + parser = argparse.ArgumentParser( + description="Test custom RLE methods (Build #220223.1003)") + + parser.add_argument( + "-b", "--bpp", + dest="bpp", type=int, + default=1, + help="Number of bits per pixel ('%(default)s' by default)") + + parser.add_argument( + "-v", "--verbose", + action='store_true', + help="Add verbosity to output ('%(default)s' by default)") + + # Call main function: + EXIT_CODE = main(parser.parse_args()) + + sys.exit(EXIT_CODE) diff --git a/speculos/mcu/screen.py b/speculos/mcu/screen.py index 4262f159..20415e18 100644 --- a/speculos/mcu/screen.py +++ b/speculos/mcu/screen.py @@ -1,12 +1,19 @@ +from __future__ import annotations + from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow from PyQt5.QtGui import QPainter, QColor, QPixmap -from PyQt5.QtGui import QIcon -from PyQt5.QtCore import Qt, QSocketNotifier, QSettings, QRect +from PyQt5.QtGui import QIcon, QKeyEvent, QMouseEvent +from PyQt5.QtCore import QEvent, Qt, QSocketNotifier, QSettings, QRect +from PyQt5.sip import voidptr +from typing import List, Optional, Union +from speculos.observer import TextEvent from . import bagl from . import nbgl -from .display import Display, DisplayArgs, FrameBuffer, COLORS, MODELS, ServerArgs +from .display import COLORS, Display, DisplayNotifier, FrameBuffer, GraphicLibrary, IODevice from .readerror import ReadError +from .struct import DisplayArgs, MODELS, ServerArgs +from .vnc import VNC BUTTON_LEFT = 1 BUTTON_RIGHT = 2 @@ -14,23 +21,24 @@ DEFAULT_WINDOW_Y = 10 -class PaintWidget(QWidget): - def __init__(self, parent, model, pixel_size, vnc=None): - super().__init__(parent) - self.fb = FrameBuffer(model) +class PaintWidget(FrameBuffer, QWidget): + def __init__(self, parent, model: str, pixel_size: int, vnc: Optional[VNC] = None): + QWidget.__init__(self, parent) + FrameBuffer.__init__(self, model) self.pixel_size = pixel_size self.mPixmap = QPixmap() self.vnc = vnc - def paintEvent(self, event): - if self.fb.pixels: + def paintEvent(self, event: QEvent): + if self.pixels or self.draw_default_color: pixmap = QPixmap(self.size() / self.pixel_size) pixmap.fill(Qt.white) painter = QPainter(pixmap) painter.drawPixmap(0, 0, self.mPixmap) self._redraw(painter) self.mPixmap = pixmap - self.fb.pixels = {} + self.pixels = {} + self.draw_default_color = False qp = QPainter(self) copied_pixmap = self.mPixmap @@ -41,47 +49,38 @@ def paintEvent(self, event): self.mPixmap.height() * self.pixel_size) qp.drawPixmap(0, 0, copied_pixmap) - def update(self, x=None, y=None, w=None, h=None) -> bool: + def update(self, # type: ignore[override] + x: Optional[int] = None, + y: Optional[int] = None, + w: Optional[int] = None, + h: Optional[int] = None) -> bool: if x and y and w and h: - super().update(QRect(x, y, w, h)) + QWidget.update(self, QRect(x, y, w, h)) else: - super().update() - return self.fb.pixels != {} + QWidget.update(self) + return self.pixels != {} def _redraw(self, qp): - for (x, y), color in self.fb.pixels.items(): + if self.draw_default_color: + qp.fillRect(0, 0, self._width, self._height, QColor.fromRgb(self.default_color)) + + for (x, y), color in self.pixels.items(): qp.setPen(QColor.fromRgb(color)) qp.drawPoint(x, y) - if self.vnc: - self.vnc.redraw(self.fb.pixels) - - self.fb.screenshot_update_pixels() - - def draw_point(self, x, y, color): - return self.fb.draw_point(x, y, color) - - def take_screenshot(self): - return self.fb.take_screenshot() - - def update_screenshot(self): - return self.fb.screenshot_update_pixels() + if self.vnc is not None: + self.vnc.redraw(self.pixels, self.default_color) - def update_public_screenshot(self): - return self.fb.update_public_screenshot() - - def get_public_screenshot(self): - return self.fb.get_public_screenshot() + self.update_screenshot() class App(QMainWindow): def __init__(self, qt_app: QApplication, display: DisplayArgs, server: ServerArgs) -> None: super().__init__() - self.setWindowTitle('Ledger %s Emulator' % MODELS[display.model].name) self.seph = server.seph - self.width, self.height = MODELS[display.model].screen_size + self._width, self._height = MODELS[display.model].screen_size self.pixel_size = display.pixel_size self.box_position_x, self.box_position_y = MODELS[display.model].box_position box_size_x, box_size_y = MODELS[display.model].box_size @@ -100,8 +99,8 @@ def __init__(self, qt_app: QApplication, display: DisplayArgs, server: ServerArg window_y = settings.value("window_y", current_screen_y + DEFAULT_WINDOW_Y, int) else: window_y = display.y - window_width = (self.width + box_size_x) * display.pixel_size - window_height = (self.height + box_size_y) * display.pixel_size + window_width = (self._width + box_size_x) * display.pixel_size + window_height = (self._height + box_size_y) * display.pixel_size # Be sure Window is FULLY visible in one of the available screens: window_is_visible = False @@ -125,7 +124,7 @@ def __init__(self, qt_app: QApplication, display: DisplayArgs, server: ServerArg self.setGeometry(window_x, window_y, window_width, window_height) self.setFixedSize(window_width, window_height) - flags = Qt.FramelessWindowHint + flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.FramelessWindowHint if display.ontop: flags |= Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint self.setWindowFlags(flags) @@ -136,48 +135,48 @@ def __init__(self, qt_app: QApplication, display: DisplayArgs, server: ServerArg self.setPalette(p) # Add paint widget and paint - self.m = PaintWidget(self, display.model, display.pixel_size, server.vnc) - self.m.move(self.box_position_x * display.pixel_size, self.box_position_y * display.pixel_size) - self.m.resize(self.width * display.pixel_size, self.height * display.pixel_size) - - self.screen = Screen(self, display, server) - + self.widget = PaintWidget(self, display.model, display.pixel_size, server.vnc) + self.widget.move(self.box_position_x * display.pixel_size, self.box_position_y * display.pixel_size) + self.widget.resize(self._width * display.pixel_size, self._height * display.pixel_size) self.setWindowIcon(QIcon('mcu/icon.png')) - self.show() + self._screen: Screen + + def set_screen(self, screen: Screen) -> None: + self._screen = screen def screen_update(self) -> bool: - return self.screen.screen_update() + return self._screen.screen_update() - def keyPressEvent(self, event): - self.screen._key_event(event, True) + def keyPressEvent(self, event: QKeyEvent): + self._screen._key_event(event, True) - def keyReleaseEvent(self, event): - self.screen._key_event(event, False) + def keyReleaseEvent(self, event: QKeyEvent): + self._screen._key_event(event, False) def _get_x_y(self): x = self.mouse_offset.x() // self.pixel_size - (self.box_position_x + 1) y = self.mouse_offset.y() // self.pixel_size - (self.box_position_y + 1) return x, y - def mousePressEvent(self, event): + def mousePressEvent(self, event: QMouseEvent): '''Get the mouse location.''' self.mouse_offset = event.pos() x, y = self._get_x_y() - if x >= 0 and x < self.width and y >= 0 and y < self.height: + if x >= 0 and x < self._width and y >= 0 and y < self._height: self.seph.handle_finger(x, y, True) QApplication.setOverrideCursor(Qt.DragMoveCursor) - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event: QMouseEvent): x, y = self._get_x_y() - if x >= 0 and x < self.width and y >= 0 and y < self.height: + if x >= 0 and x < self._width and y >= 0 and y < self._height: self.seph.handle_finger(x, y, False) QApplication.restoreOverrideCursor() - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event: QMouseEvent): '''Move the window.''' x = event.globalX() @@ -186,7 +185,7 @@ def mouseMoveEvent(self, event): y_w = self.mouse_offset.y() self.move(x - x_w, y - y_w) - def closeEvent(self, event): + def closeEvent(self, event: QEvent): ''' Called when the window is closed. We save the current window position to the settings file in order to restore it upon next speculos execution. @@ -197,45 +196,30 @@ def closeEvent(self, event): class Screen(Display): - def __init__(self, app: App, display: DisplayArgs, server: ServerArgs) -> None: - self.app = app + def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: super().__init__(display, server) - self._init_notifiers(server) - if display.model != "stax": - self.bagl = bagl.Bagl(app.m, MODELS[display.model].screen_size) - else: - self.nbgl = nbgl.NBGL(app.m, MODELS[display.model].screen_size, display.force_full_ocr, - display.disable_tesseract) - self.seph = server.seph - - def klass_can_read(self, klass, s): - try: - klass.can_read(s, self) - - # This exception occur when can_read have no more data available - except ReadError: - self.app.close() - - def add_notifier(self, klass): - n = QSocketNotifier(klass.s.fileno(), QSocketNotifier.Read, self.app) - n.activated.connect(lambda s: self.klass_can_read(klass, s)) + self.app: App + self._gl: GraphicLibrary - assert klass.s.fileno() not in self.notifiers - self.notifiers[klass.s.fileno()] = n - - def enable_notifier(self, fd, enabled=True): - n = self.notifiers[fd] - n.setEnabled(enabled) + def set_app(self, app: App) -> None: + self.app = app + self.app.set_screen(self) + model = self._display_args.model + if model != "stax": + self._gl = bagl.Bagl(app.widget, MODELS[model].screen_size, model) + else: + self._gl = nbgl.NBGL(app.widget, MODELS[model].screen_size, model) - def remove_notifier(self, fd): - # just in case - self.enable_notifier(fd, False) + @property + def m(self) -> QWidget: + return self.app.widget - n = self.notifiers.pop(fd) - n.disconnect() + @property + def gl(self): + return self._gl - def _key_event(self, event, pressed): - key = event.key() + def _key_event(self, event: QKeyEvent, pressed) -> None: + key = Qt.Key(event.key()) if key in [Qt.Key_Left, Qt.Key_Right]: buttons = {Qt.Key_Left: BUTTON_LEFT, Qt.Key_Right: BUTTON_RIGHT} # forward this event to seph @@ -246,27 +230,55 @@ def _key_event(self, event, pressed): elif key == Qt.Key_Q and not pressed: self.app.close() - def display_status(self, data): - ret = self.bagl.display_status(data) + def display_status(self, data: bytes) -> List[TextEvent]: + assert isinstance(self.gl, bagl.Bagl) + ret = self.gl.display_status(data) if MODELS[self.model].name == 'blue': self.screen_update() # Actually, this method doesn't work return ret - def display_raw_status(self, data): - self.bagl.display_raw_status(data) + def display_raw_status(self, data: bytes) -> None: + assert isinstance(self.gl, bagl.Bagl) + self.gl.display_raw_status(data) if MODELS[self.model].name == 'blue': self.screen_update() # Actually, this method doesn't work def screen_update(self) -> bool: - return self.bagl.refresh() + return self.gl.refresh() -class QtScreen: - def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: - self.app = QApplication([]) - self.app_widget = App(self.app, display, server) - self.m = self.app_widget.m +class QtScreenNotifier(DisplayNotifier): + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + self._qapp = QApplication([]) + super().__init__(display_args, server_args) + self._set_display_class(Screen) + self._app_widget = App(self._qapp, display_args, server_args) + assert isinstance(self.display, Screen) + self.display.set_app(self._app_widget) + + def _can_read(self, device: IODevice) -> None: + try: + device.can_read(self) + # This exception occur when can_read have no more data available + except ReadError: + self._app_widget.close() + + def add_notifier(self, device: IODevice) -> None: + n = QSocketNotifier(voidptr(device.fileno), QSocketNotifier.Read, self._qapp) + n.activated.connect(lambda _: self._can_read(device)) + assert device.fileno not in self.notifiers + self.notifiers[device.fileno] = n + + def enable_notifier(self, fd: int, enabled: bool = True) -> None: + n = self.notifiers[fd] + n.setEnabled(enabled) + + def remove_notifier(self, fd: int) -> None: + # just in case + self.enable_notifier(fd, False) + n = self.notifiers.pop(fd) + n.disconnect() def run(self): - self.app.exec_() - self.app.quit() + self._qapp.exec_() + self._qapp.quit() diff --git a/speculos/mcu/screen_text.py b/speculos/mcu/screen_text.py index b5e1d4f9..ea509b14 100644 --- a/speculos/mcu/screen_text.py +++ b/speculos/mcu/screen_text.py @@ -1,13 +1,15 @@ -import sys -import select import curses import logging import os +import select +import sys +import time +from typing import Any, List from . import bagl -from .display import Display, DisplayArgs, FrameBuffer, MODELS, ServerArgs +from .display import Display, DisplayNotifier, FrameBuffer, MODELS from .readerror import ReadError -import time +from .struct import DisplayArgs, ServerArgs wait_time = 0.01 @@ -18,7 +20,7 @@ _BORDER_ = "\033[30;1;40m" _RESET_COLOR = "\033[0m" -M = [0]*16 +M: List = [0]*16 M[0b0000] = ' ' M[0b0001] = '\u2598' M[0b0010] = '\u259D' @@ -44,7 +46,7 @@ def map_pix(a, b, c, d): class TextWidget(FrameBuffer): - def __init__(self, parent, model): + def __init__(self, parent, model: str): super().__init__(model) self.width = parent.width self.height = parent.height @@ -61,9 +63,9 @@ def __init__(self, parent, model): curses.start_color() curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) - self.stdscr.nodelay(1) # returns -1 if nothing + self.stdscr.nodelay(True) # returns -1 if nothing self.stdscr.clear() - self.stdscr.keypad(1) # interpret escape sequences generated by keypad and function keys + self.stdscr.keypad(True) # interpret escape sequences generated by keypad and function keys def get_pixel(self, x, y): color = self.pixels.get((x, y), 0) @@ -94,21 +96,22 @@ def _redraw(self): self.stdscr.addstr(0, 0, ' '*(self.width//2 + 2), curses.color_pair(2)) self.stdscr.addstr(self.height//2+1, 0, ' '*(self.width//2 + 2), curses.color_pair(2)) self.stdscr.refresh() - self.screenshot_update_pixels() + self.update_screenshot() class TextScreen(Display): - def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: - super().__init__(display, server) + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + super().__init__(display_args, server_args) - self.width, self.height = MODELS[display.model].screen_size - self.m = TextWidget(self, display.model) - self.bagl = bagl.Bagl(self.m, MODELS[display.model].screen_size) - - self._init_notifiers(server) + self.width, self.height = MODELS[display_args.model].screen_size + self.m = TextWidget(self, display_args.model) + if display_args.model != "stax": + self._gl = bagl.Bagl(self.m, MODELS[display_args.model].screen_size, display_args.model) + else: + raise NotImplementedError("This display can not emulate NBGL OS yet") - if display.keymap is not None: - self.ARROW_KEYS = list(map(ord, display.keymap)) + if display_args.keymap is not None: + self.ARROW_KEYS = list(map(ord, display_args.keymap)) else: self.ARROW_KEYS = [curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_DOWN] @@ -118,32 +121,43 @@ def __init__(self, display: DisplayArgs, server: ServerArgs) -> None: self.ARROW_KEYS[2]: BUTTON_LEFT | BUTTON_RIGHT, } + @property + def gl(self) -> bagl.Bagl: + return self._gl + def display_status(self, data): - return self.bagl.display_status(data) + return self.gl.display_status(data) - def display_raw_status(self, data): - self.bagl.display_raw_status(data) + def display_raw_status(self, data) -> None: + self.gl.display_raw_status(data) def screen_update(self) -> bool: - return self.bagl.refresh() + return self.gl.refresh() - def get_keypress(self): + def get_keypress(self) -> bool: key = self.m.stdscr.getch() if key == -1: return True elif key in self.ARROW_KEYS: - self.seph.handle_button(self.key2btn[key], 1) + self.seph.handle_button(self.key2btn[key], True) time.sleep(wait_time) - self.seph.handle_button(self.key2btn[key], 0) + self.seph.handle_button(self.key2btn[key], False) return True elif key == ord('q'): return False else: return True - def run(self): + +class TextScreenNotifier(DisplayNotifier): + + def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None: + super().__init__(display_args, server_args) + self._set_display_class(TextScreen) + + def run(self) -> None: while True: - rlist = list(self.notifiers.keys()) + rlist: List[Any] = list(self.notifiers.keys()) if not rlist: break @@ -151,11 +165,12 @@ def run(self): rlist, _, _ = select.select(rlist, [], []) if sys.stdin in rlist: rlist.remove(sys.stdin) - if not self.get_keypress(): + assert isinstance(self.display, TextScreen) + if not self.display.get_keypress(): break try: for fd in rlist: - self.notifiers[fd].can_read(fd, self) + self.notifiers[fd].can_read(self) # This exception occur when can_read have no more data available except ReadError: diff --git a/speculos/mcu/seproxyhal.py b/speculos/mcu/seproxyhal.py index 84529b6c..9f64af0a 100644 --- a/speculos/mcu/seproxyhal.py +++ b/speculos/mcu/seproxyhal.py @@ -1,17 +1,19 @@ -from collections import namedtuple import logging import sys -import time import threading -import socket +import time +from collections import namedtuple from enum import IntEnum -from typing import List, Callable, Optional, Tuple +from socket import socket +from typing import Callable, List, Optional, Tuple +from speculos.observer import BroadcastInterface, TextEvent from . import usb +from .automation import Automation +from .display import DisplayNotifier, IODevice +from .nbgl import NBGL from .ocr import OCR -from .readerror import ReadError, WriteError -from .automation import Automation, TextEvent -from .automation_server import AutomationServer +from .readerror import ReadError class SephTag(IntEnum): @@ -32,6 +34,7 @@ class SephTag(IntEnum): RAPDU = 0x53 PLAY_TUNE = 0x56 + DBG_SCREEN_DISPLAY_STATUS = 0x5e PRINTC_STATUS = 0x5f GENERAL_STATUS = 0x60 @@ -43,6 +46,18 @@ class SephTag(IntEnum): FINGER_EVENT_TOUCH = 0x01 FINGER_EVENT_RELEASE = 0x02 + # Speculos only, defined in speculos/src/bolos/bagl.c + BAGL_DRAW_RECT = 0xF1 + BAGL_DRAW_BITMAP = 0xF2 + + # Speculos only, defined in speculos/src/bolos/nbgl.c + NBGL_DRAW_RECT = 0xFA + NBGL_REFRESH = 0xFB + NBGL_DRAW_LINE = 0xFC + NBGL_DRAW_IMAGE = 0xFD + NBGL_DRAW_IMAGE_FILE = 0xFE + NBGL_DRAW_IMAGE_RLE = 0xFF + TICKER_DELAY = 0.1 @@ -62,6 +77,7 @@ def __init__(self, add_tick: Callable, *args, **kwargs): :type add_tick: Backend """ super().__init__(name="time_ticker", daemon=True) + self.paused = False self._paused = False self._resume_cond = threading.Condition() self.add_tick = add_tick @@ -70,13 +86,18 @@ def pause(self): """ Pause time emulation done by the daemon, no ticker event will be sent until resume """ - self._paused = True + self.paused = True + + # Wait until the daemon is really paused before returning. + # To make sure last daemon tick has been sent and fully processed. + while not self._paused: + time.sleep(0.1) def resume(self): """ - Resume time emulation done by the daemon, no ticker event will be sent until resume + Resume time emulation done by the daemon """ - self._paused = False + self.paused = False with self._resume_cond: self._resume_cond.notify() @@ -84,9 +105,11 @@ def _wait_if_time_paused(self): """ Internal function to handle the pause """ - while self._paused: + while self.paused: + self._paused = True with self._resume_cond: self._resume_cond.wait() + self._paused = False def run(self): """ @@ -98,29 +121,65 @@ def run(self): time.sleep(TICKER_DELAY) -class PacketDaemon(threading.Thread): - def __init__(self, s: socket.socket, status_event: threading.Event, *args, **kwargs): +class SocketHelper(threading.Thread): + def __init__(self, sock: socket, status_event: threading.Event, *args, **kwargs): super().__init__(name="packet", daemon=True) - self.s = s + self.socket = sock + self.sending_lock = threading.Lock() self.queue_condition = threading.Condition() self.queue: List[Tuple[SephTag, bytes]] = [] self.status_event = status_event self.logger = logging.getLogger("seproxyhal.packet") self.stop = False self.ticks_count = 0 + self.tick_requested = False + + def _recvall(self, size: int): + data = b'' + while size > 0: + try: + tmp = self.socket.recv(size) + except ConnectionResetError: + tmp = b'' + + if len(tmp) == 0: + self.logger.debug("fd closed") + return None + data += tmp + size -= len(tmp) + return data + + def get_tick_count(self): + return self.ticks_count + + def read_packet(self): + data = self._recvall(3) + if data is None: + return None, None, None + + tag = data[0] + size = int.from_bytes(data[1:3], 'big') + + data = self._recvall(size) + if data is None: + return None, None, None + assert len(data) == size + + return tag, size, data - def _send_packet(self, tag: SephTag, data: bytes = b''): + def send_packet(self, tag: SephTag, data: bytes = b''): """Send a packet to the app.""" size: bytes = len(data).to_bytes(2, 'big') packet: bytes = tag.to_bytes(1, 'big') + size + data - self.logger.debug("send {}" .format(packet.hex())) - try: - self.s.sendall(packet) - except BrokenPipeError: - self.stop = True - except OSError: - self.stop = True + with self.sending_lock: + self.logger.debug("send {}" .format(packet.hex())) + try: + self.socket.sendall(packet) + except BrokenPipeError: + self.stop = True + except OSError: + self.stop = True def queue_packet(self, tag: SephTag, data: bytes = b'', priority: bool = False): """ @@ -140,52 +199,56 @@ def queue_packet(self, tag: SephTag, data: bytes = b'', priority: bool = False): with self.queue_condition: self.queue_condition.notify() - def add_tick(self): - """Add a ticker event to the queue.""" + def add_tick(self, wait_fully_processed=False): + """Request sending of a ticker event to the app""" + self.tick_requested = True - # Drop ticker packet if one is already present in the queue. - # For instance, the app might be stuck if a breakpoint is hit within a debugger. - # It avoids flooding the app on resume. - for tag, _ in self.queue: - if tag == SephTag.TICKER_EVENT: - return False + # notify this thread that a new event is available + with self.queue_condition: + self.queue_condition.notify() - self.queue_packet(SephTag.TICKER_EVENT) - return True + if wait_fully_processed: + # Wait until the app have finished processing the tick + while self.tick_requested or self.queue or not self.status_event.is_set(): + self.status_event.wait() def run(self): while not self.stop: + + # wait for a event in the queue or a tick to be available + with self.queue_condition: + while len(self.queue) == 0 and not self.tick_requested: + self.queue_condition.wait() + # wait for a status notification while not self.status_event.is_set(): self.status_event.wait() self.status_event.clear() - # wait for a packet to be available in the queue - with self.queue_condition: - while len(self.queue) == 0: - self.queue_condition.wait() + if len(self.queue): + tag, data = self.queue.pop(0) + elif self.tick_requested: + tag, data = SephTag.TICKER_EVENT, b'' + else: + raise RuntimeError("Unexpected state: no ticker nor event to send on socket") - tag, data = self.queue.pop(0) - self._send_packet(tag, data) + self.send_packet(tag, data) if tag == SephTag.TICKER_EVENT: self.ticks_count += 1 + self.tick_requested = False self.logger.debug("exiting") - def get_processed_ticks_count(self): - return self.ticks_count - -class SeProxyHal: +class SeProxyHal(IODevice): def __init__(self, - s: socket.socket, + sock: socket, + model: str, automation: Optional[Automation] = None, - automation_server: Optional[AutomationServer] = None, - transport: str = 'hid', - fonts_path: str = None, - api_level=None): - self.s = s + automation_server: Optional[BroadcastInterface] = None, + transport: str = 'hid'): + self._socket = sock self.logger = logging.getLogger("seproxyhal") self.printf_queue = '' self.automation = automation @@ -194,43 +257,22 @@ def __init__(self, self.refreshed = False self.status_event = threading.Event() - self.packet_thread = PacketDaemon(self.s, self.status_event) - self.packet_thread.start() + self.socket_helper = SocketHelper(self._socket, self.status_event) + self.socket_helper.start() - self.time_ticker_thread = TimeTickerDaemon(self.packet_thread.add_tick) + self.time_ticker_thread = TimeTickerDaemon(self.socket_helper.add_tick) self.time_ticker_thread.start() - self.usb = usb.USB(self.packet_thread.queue_packet, transport=transport) + self.usb = usb.USB(self.socket_helper.queue_packet, transport=transport) - self.ocr = OCR(fonts_path, api_level) + self.ocr = OCR(model) # A list of callback methods when an APDU response is received - self.apdu_callbacks: List[Callable] = [] - - def _recvall(self, size: int): - data = b'' - while size > 0: - try: - tmp = self.s.recv(size) - except ConnectionResetError: - tmp = b'' - - if len(tmp) == 0: - self.logger.debug("fd closed") - return None - data += tmp - size -= len(tmp) - return data - - def _send_packet(self, tag: SephTag, data: bytes = b''): - '''Send packet to the app.''' + self.apdu_callbacks: List[Callable[[bytes], None]] = [] - size = len(data).to_bytes(2, 'big') - packet = tag.to_bytes(1, 'big') + size + data - try: - self.s.sendall(packet) - except BrokenPipeError: - raise WriteError("Broken pipe, failed to send data to the app") + @property + def file(self): + return self._socket def apply_automation_helper(self, event: TextEvent): if self.automation_server: @@ -248,7 +290,7 @@ def apply_automation_helper(self, event: TextEvent): elif key == "setbool": self.automation.set_bool(*args) elif key == "exit": - self.s.close() + self.file.close() sys.exit(0) else: assert False @@ -258,130 +300,83 @@ def apply_automation(self): self.apply_automation_helper(event) self.events = [] - def _close(self, s: socket.socket, screen): - screen.remove_notifier(self.s.fileno()) - self.s.close() + def _cleanup(self, notifier: DisplayNotifier): + notifier.remove_notifier(self.fileno) + self.file.close() - def can_read(self, s: socket.socket, screen): + def can_read(self, screen: DisplayNotifier): ''' Handle packet sent by the app. This function is called thanks to a screen QSocketNotifier. ''' - - assert s == self.s.fileno() - - data = self._recvall(3) - if data is None: - self._close(s, screen) - raise ReadError("fd closed") - - tag = data[0] - size = int.from_bytes(data[1:3], 'big') - - data = self._recvall(size) + tag, size, data = self.socket_helper.read_packet() if data is None: - self._close(s, screen) + self._cleanup(screen) raise ReadError("fd closed") - assert len(data) == size self.logger.debug(f"received (tag: {tag:#04x}, size: {size:#04x}): {data!r}") - if tag & 0xf0 == SephTag.GENERAL_STATUS or tag == SephTag.PRINTC_STATUS: - - if tag == SephTag.GENERAL_STATUS: - if int.from_bytes(data[:2], 'big') == SephTag.GENERAL_STATUS_LAST_COMMAND: - if self.refreshed: - self.refreshed = False - - if not screen.nbgl.disable_tesseract: - # Pause flow of time while the OCR is running - self.time_ticker_thread.pause() + if tag == SephTag.GENERAL_STATUS: + if int.from_bytes(data[:2], 'big') == SephTag.GENERAL_STATUS_LAST_COMMAND: + if self.refreshed: + self.refreshed = False - # Run the OCR - screen.nbgl.m.update_screenshot() - screen_size, image_data = screen.nbgl.m.take_screenshot() - self.ocr.analyze_image(screen_size, image_data) + # Update the screenshot, we'll upload its associated events shortly + screen.display.gl.update_screenshot() + screen.display.gl.update_public_screenshot() - # Publish the new screenshot, we'll upload its associated events shortly - screen.nbgl.m.update_public_screenshot() - - # OCR is finished, resume time - self.time_ticker_thread.resume() - - if screen.model != "stax" and screen.screen_update(): - if screen.model in ["nanox", "nanosp"]: - self.events += self.ocr.get_events() - elif screen.model == "stax": + if screen.display.model != "stax" and screen.display.screen_update(): + if screen.display.model in ["nanox", "nanosp"]: self.events += self.ocr.get_events() + elif screen.display.model == "stax": + self.events += self.ocr.get_events() - # Apply automation rules after having received a GENERAL_STATUS_LAST_COMMAND tag. It allows the - # screen to be updated before broadcasting the events. - if self.events: - self.apply_automation() + # Apply automation rules after having received a GENERAL_STATUS_LAST_COMMAND tag. It allows the + # screen to be updated before broadcasting the events. + if self.events: + self.apply_automation() + + # signal the sending thread that a status has been received + self.status_event.set() - elif tag == SephTag.SCREEN_DISPLAY_STATUS: - self.logger.debug(f"DISPLAY_STATUS {data!r}") - events = screen.display_status(data) - if events: - self.events += events - self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) - - elif tag == SephTag.SCREEN_DISPLAY_RAW_STATUS: - self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") - screen.display_raw_status(data) - if screen.model in ["nanox", "nanosp"]: - # Pause flow of time while the OCR is running - self.time_ticker_thread.pause() - self.ocr.analyze_bitmap(data) - self.time_ticker_thread.resume() - # https://github.com/LedgerHQ/nanos-secure-sdk/blob/1f2706941b68d897622f75407a868b60eb2be8d7/src/os_io_seproxyhal.c#L787 - # - # io_seproxyhal_spi_recv() accepts any packet from the MCU after - # having sent SCREEN_DISPLAY_RAW_STATUS. If some event (eg. - # TICKER_EVENT) is replied before DISPLAY_PROCESSED_EVENT, it - # will be silently ignored. - # - # A DISPLAY_PROCESSED_EVENT should be answered immediately, - # hence priority=True. - self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) - if screen.rendering == RENDER_METHOD.PROGRESSIVE: - screen.screen_update() - - elif tag == SephTag.PRINTF_STATUS or tag == SephTag.PRINTC_STATUS: - for b in [chr(b) for b in data]: - if b == '\n': - self.logger.info(f"printf: {self.printf_queue}") - self.printf_queue = '' - else: - self.printf_queue += b - self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) - if screen.rendering == RENDER_METHOD.PROGRESSIVE: - screen.screen_update() - elif tag == 0x6a: - screen.nbgl.hal_draw_rect(data) - elif tag == 0x6b: - screen.nbgl.hal_refresh(data) - # Stax only - # We have refreshed the screen, remember it for the next time we have SephTag.GENERAL_STATUS - # then we'll perform a new OCR and make public the resulting screenshot / OCR analysis - self.refreshed = True - - elif tag == 0x6c: - screen.nbgl.hal_draw_line(data) - elif tag == 0x6d: - screen.nbgl.hal_draw_image(data) - elif tag == 0x6e: - screen.nbgl.hal_draw_image_file(data) else: - self.logger.error(f"unknown tag: {tag:#x}") + self.logger.error(f"unknown subtag: {data[:2]!r}") sys.exit(0) - # signal the sending thread that a status has been received - self.status_event.set() + elif tag in [SephTag.SCREEN_DISPLAY_STATUS, + SephTag.DBG_SCREEN_DISPLAY_STATUS, + SephTag.BAGL_DRAW_RECT]: + self.logger.debug(f"DISPLAY_STATUS {data!r}") + if screen.display.model not in ["nanox", "nanosp"] or tag == SephTag.BAGL_DRAW_RECT: + events = screen.display.display_status(data) + if events: + self.events += events + if tag != SephTag.BAGL_DRAW_RECT: + self.socket_helper.send_packet(SephTag.DISPLAY_PROCESSED_EVENT) + + elif tag in [SephTag.SCREEN_DISPLAY_RAW_STATUS, SephTag.BAGL_DRAW_BITMAP]: + self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") + screen.display.display_raw_status(data) + if screen.display.model in ["nanox", "nanosp"]: + self.ocr.analyze_bitmap(data) + if tag != SephTag.BAGL_DRAW_BITMAP: + self.socket_helper.send_packet(SephTag.DISPLAY_PROCESSED_EVENT) + if screen.display.rendering == RENDER_METHOD.PROGRESSIVE: + screen.display.screen_update() + + elif tag == SephTag.PRINTF_STATUS or tag == SephTag.PRINTC_STATUS: + for b in [chr(b) for b in data]: + if b == '\n': + self.logger.info(f"printf: {self.printf_queue}") + self.printf_queue = '' + else: + self.printf_queue += b + if screen.display.model in ["blue"]: + self.socket_helper.send_packet(SephTag.DISPLAY_PROCESSED_EVENT) elif tag == SephTag.RAPDU: - screen.forward_to_apdu_client(data) + screen.display.forward_to_apdu_client(data) for c in self.apdu_callbacks: c(data) @@ -393,7 +388,7 @@ def can_read(self, s: socket.socket, screen): if data: for c in self.apdu_callbacks: c(data) - screen.forward_to_apdu_client(data) + screen.display.forward_to_apdu_client(data) elif tag == SephTag.MCU: pass @@ -406,7 +401,7 @@ def can_read(self, s: socket.socket, screen): elif tag == SephTag.SE_POWER_OFF: self.logger.warn("received tag SE_POWER_OFF, exiting") - self._close(s, screen) + self._cleanup(screen) raise ReadError("SE_POWER_OFF") elif tag == SephTag.REQUEST_STATUS: @@ -416,6 +411,36 @@ def can_read(self, s: socket.socket, screen): elif tag == SephTag.PLAY_TUNE: pass + elif tag == SephTag.NBGL_DRAW_RECT: + assert isinstance(screen.display.gl, NBGL) + screen.display.gl.hal_draw_rect(data) + + elif tag == SephTag.NBGL_REFRESH: + assert isinstance(screen.display.gl, NBGL) + screen.display.gl.refresh(data) + # Stax only + # We have refreshed the screen, remember it for the next time we have SephTag.GENERAL_STATUS + # then we'll perform a screen update and make public the resulting screenshot + self.refreshed = True + + elif tag == SephTag.NBGL_DRAW_LINE: + assert isinstance(screen.display.gl, NBGL) + screen.display.gl.hal_draw_line(data) + + elif tag == SephTag.NBGL_DRAW_IMAGE: + assert isinstance(screen.display.gl, NBGL) + self.ocr.analyze_bitmap(data) + screen.display.gl.hal_draw_image(data) + + elif tag == SephTag.NBGL_DRAW_IMAGE_RLE: + assert isinstance(screen.display.gl, NBGL) + self.ocr.analyze_bitmap(data) + screen.display.gl.hal_draw_image_rle(data) + + elif tag == SephTag.NBGL_DRAW_IMAGE_FILE: + assert isinstance(screen.display.gl, NBGL) + screen.display.gl.hal_draw_image_file(data) + else: self.logger.error(f"unknown tag: {tag:#x}") sys.exit(0) @@ -424,9 +449,9 @@ def handle_button(self, button: int, pressed: bool): '''Forward button press/release from the GUI to the app.''' if pressed: - self.packet_thread.queue_packet(SephTag.BUTTON_PUSH_EVENT, (button << 1).to_bytes(1, 'big')) + self.socket_helper.queue_packet(SephTag.BUTTON_PUSH_EVENT, (button << 1).to_bytes(1, 'big')) else: - self.packet_thread.queue_packet(SephTag.BUTTON_PUSH_EVENT, (0 << 1).to_bytes(1, 'big')) + self.socket_helper.queue_packet(SephTag.BUTTON_PUSH_EVENT, (0 << 1).to_bytes(1, 'big')) def handle_finger(self, x: int, y: int, pressed: bool): '''Forward finger press/release from the GUI to the app.''' @@ -438,13 +463,26 @@ def handle_finger(self, x: int, y: int, pressed: bool): packet += x.to_bytes(2, 'big') packet += y.to_bytes(2, 'big') - self.packet_thread.queue_packet(SephTag.FINGER_EVENT, packet) + self.socket_helper.queue_packet(SephTag.FINGER_EVENT, packet) + + def handle_ticker_request(self, action): + if action == "pause": + self.time_ticker_thread.pause() + elif action == "resume": + self.time_ticker_thread.resume() + elif action == "single-step": + self.time_ticker_thread.add_tick(wait_fully_processed=True) def handle_wait(self, delay: float): '''Wait for a specified delay, taking account real time seen by the app.''' - start = self.packet_thread.get_processed_ticks_count() - while (self.packet_thread.get_processed_ticks_count() - start) * TICKER_DELAY < delay: - time.sleep(TICKER_DELAY) + expected_ticks = int(delay / TICKER_DELAY) + if not self.time_ticker_thread.paused: + start = self.socket_helper.ticks_count + while (self.socket_helper.ticks_count - start) < expected_ticks: + time.sleep(TICKER_DELAY) + else: + for _ in range(expected_ticks): + self.time_ticker_thread.add_tick(wait_fully_processed=True) def to_app(self, packet: bytes): ''' @@ -458,6 +496,9 @@ def to_app(self, packet: bytes): if packet.startswith(b'RAW!') and len(packet) > 4: tag, packet = packet[4], packet[5:] - self.packet_thread.queue_packet(tag, packet) + self.socket_helper.queue_packet(SephTag(tag), packet) else: self.usb.xfer(packet) + + def get_tick_count(self): + return self.socket_helper.get_tick_count() diff --git a/speculos/mcu/struct.py b/speculos/mcu/struct.py new file mode 100644 index 00000000..b122b924 --- /dev/null +++ b/speculos/mcu/struct.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass +from typing import Any, Dict, NamedTuple, Optional, Tuple + +Pixel = Tuple[int, int] + + +@dataclass +class Model: + name: str + screen_size: Tuple[int, int] + box_position: Pixel + box_size: Tuple[int, int] + + +MODELS: Dict[str, Model] = { + 'nanos': Model('Nano S', (128, 32), (20, 13), (100, 26)), + 'nanox': Model('Nano X', (128, 64), (5, 5), (10, 10)), + 'nanosp': Model('Nano SP', (128, 64), (5, 5), (10, 10)), + 'blue': Model('Blue', (320, 480), (13, 13), (26, 26)), + 'stax': Model('Stax', (400, 672), (13, 13), (26, 26)), +} + + +@dataclass +class DisplayArgs: + color: str + model: str + ontop: bool + rendering: bool + keymap: str + pixel_size: int + x: Optional[int] + y: Optional[int] + + +class ServerArgs(NamedTuple): + apdu: Any # ApduServer + apirun: Any # Optional[ApiRunner] + button: Any # Optional[FakeButton] + finger: Any # Optional[FakeFinger] + seph: Any # SeProxyHal + vnc: Any # Optional[VNC] diff --git a/speculos/mcu/vnc.py b/speculos/mcu/vnc.py index e974f96e..4df38f10 100644 --- a/speculos/mcu/vnc.py +++ b/speculos/mcu/vnc.py @@ -9,19 +9,26 @@ import logging import subprocess import sys +from typing import IO, Optional, Tuple +from .display import DisplayNotifier, IODevice, PixelColorMapping -class VNC: - def __init__(self, port, screen_size, password=None, verbose=False): + +class VNC(IODevice): + def __init__(self, + port: int, + screen_size: Tuple[int, int], + password: Optional[str] = None, + verbose: bool = False): self.logger = logging.getLogger("vnc") - width, height = screen_size + self._width, self._height = screen_size path = os.path.dirname(os.path.realpath(__file__)) server = os.path.join(path, '../resources/vnc_server') cmd = [server] # custom options - cmd += ['-s', f'{width}x{height}'] + cmd += ['-s', f'{self._width}x{self._height}'] if verbose: cmd += ['-v'] @@ -34,41 +41,43 @@ def __init__(self, port, screen_size, password=None, verbose=False): if password is not None: cmd += ['-passwd', password] - self.p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + self.subprocess = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - # required by Screen.add_notifier - self.s = self.p.stdout + @property + def file(self) -> IO[bytes]: + assert self.subprocess.stdout is not None + return self.subprocess.stdout - def redraw(self, pixels): + def redraw(self, pixels: PixelColorMapping, default_color: int) -> None: '''The framebuffer was updated, forward everything to the VNC server.''' # int.to_bytes() is super slow, hence the manual encoding buf = bytearray(len(pixels) * 9) i = 0 - for (x, y), color in pixels.items(): - buf[i + 0] = y & 0xff - buf[i + 1] = (y >> 8) & 0xff - buf[i + 2] = x & 0xff - buf[i + 3] = (x >> 8) & 0xff - buf[i + 4] = color & 0xff - buf[i + 5] = (color >> 8) & 0xff - buf[i + 6] = (color >> 16) & 0xff - buf[i + 7] = (color >> 24) & 0xff - buf[i + 8] = 0x0a - i += 9 - - self.p.stdin.write(buf) - self.p.stdin.flush() - - def can_read(self, s, screen): + for x in range(0, self._width): + for y in range(0, self._height): + color = pixels.get((x, y), default_color) + buf[i + 0] = y & 0xff + buf[i + 1] = (y >> 8) & 0xff + buf[i + 2] = x & 0xff + buf[i + 3] = (x >> 8) & 0xff + buf[i + 4] = color & 0xff + buf[i + 5] = (color >> 8) & 0xff + buf[i + 6] = (color >> 16) & 0xff + buf[i + 7] = (color >> 24) & 0xff + buf[i + 8] = 0x0a + i += 9 + + assert self.subprocess.stdin is not None + self.subprocess.stdin.write(buf) + self.subprocess.stdin.flush() + + def can_read(self, screen: DisplayNotifier) -> None: '''Process a new keyboard or mouse event message from the VNC server.''' - assert s == self.s.fileno() - assert s == self.p.stdout.fileno() - data = b'' while len(data) != 6: - tmp = self.p.stdout.read(6 - len(data)) + tmp = self.file.read(6 - len(data)) if not tmp: self.logger.info("connection with vnc stdout closed") sys.exit(0) @@ -79,11 +88,11 @@ def can_read(self, s, screen): x = int.from_bytes(data[0:2], 'little') y = int.from_bytes(data[2:4], 'little') pressed = (data[4] != 0x00) - screen.seph.handle_finger(x, y, pressed) + screen.display.seph.handle_finger(x, y, pressed) elif data[4] in [0x10, 0x11]: # keyboard button = data[0] pressed = (data[4] == 0x11) - screen.seph.handle_button(button, pressed) + screen.display.seph.handle_button(button, pressed) else: self.logger.error("invalid message from the VNC server") diff --git a/speculos/observer.py b/speculos/observer.py new file mode 100644 index 00000000..30029d3d --- /dev/null +++ b/speculos/observer.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from logging import Logger +from typing import List + + +@dataclass +class TextEvent: + text: str + x: int + y: int + w: int + h: int + + +class ObserverInterface(ABC): + + @abstractmethod + def send_screen_event(self, event: TextEvent) -> None: + pass + + +class BroadcastInterface(ABC): + + def __init__(self) -> None: + self.logger: Logger + self.clients: List[ObserverInterface] = list() + + def add_client(self, client: ObserverInterface) -> None: + self.logger.debug("New client '%s'", client) + self.clients.append(client) + + def remove_client(self, client: ObserverInterface) -> None: + self.logger.debug("Removing client '%s'", client) + self.clients.remove(client) + + @abstractmethod + def broadcast(self, event: TextEvent) -> None: + pass diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 90e4aede..34dc0f10 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(emu bolos/cx_weierstrass.c bolos/cxlib.c bolos/bagl.c + bolos/fonts_info.c bolos/nbgl.c bolos/nbgl_rle.c bolos/touch.c @@ -55,6 +56,7 @@ add_library(emu emulate_blue_2.2.5.c emulate_lnsp_1.0.c emulate_unified_sdk.c + environment.c svc.c) add_dependencies(emu openssl) diff --git a/src/bolos/bagl.c b/src/bolos/bagl.c index 53b3972f..04b2ab16 100644 --- a/src/bolos/bagl.c +++ b/src/bolos/bagl.c @@ -3,12 +3,18 @@ #include "bagl.h" #include "emulate.h" +#include "fonts.h" -#define SEPROXYHAL_TAG_SCREEN_DISPLAY_STATUS 0x65 -#define SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS 0x69 #define SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START 0x00 #define SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_CONT 0x01 +#define SEPROXYHAL_TAG_BAGL_DRAW_RECT 0xF1 +#define SEPROXYHAL_TAG_BAGL_DRAW_BITMAP 0xF2 +#define SEPROXYHAL_TAG_BAGL_DRAW_BITMAP_START \ + SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START +#define SEPROXYHAL_TAG_BAGL_DRAW_BITMAP_CONT \ + SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_CONT + #define BAGL_FILL 1 #define BAGL_RECTANGLE 3 @@ -51,7 +57,7 @@ unsigned long sys_bagl_hal_draw_rect(unsigned int color, int x, int y, len = sizeof(c); - header[0] = SEPROXYHAL_TAG_SCREEN_DISPLAY_STATUS; + header[0] = SEPROXYHAL_TAG_BAGL_DRAW_RECT; header[1] = (len >> 8) & 0xff; header[2] = len & 0xff; @@ -67,6 +73,14 @@ unsigned long sys_bagl_hal_draw_rect(unsigned int color, int x, int y, dst[offset++] = value & 0xff; \ } while (0) +#define IBE(value, dst, offset) \ + do { \ + dst[offset++] = (value >> 24) & 0xff; \ + dst[offset++] = (value >> 16) & 0xff; \ + dst[offset++] = (value >> 8) & 0xff; \ + dst[offset++] = value & 0xff; \ + } while (0) + size_t build_chunk(uint8_t *buf, size_t *offset, size_t size, const uint8_t *bitmap, const size_t bitmap_length) { @@ -87,7 +101,7 @@ unsigned long sys_bagl_hal_draw_bitmap_within_rect( size_t i, len, size; uint8_t buf[300 - 4]; /* size limit in io_seph_send */ uint8_t header[4]; - + uint32_t character = get_character_from_bitmap(bitmap); size = 0; HBE(x, buf, size); @@ -97,6 +111,8 @@ unsigned long sys_bagl_hal_draw_bitmap_within_rect( buf[size++] = bit_per_pixel; + IBE(character, buf, size); + if (size + color_count * sizeof(unsigned int) > sizeof(buf)) { errx(1, "color overflow in sys_bagl_hal_draw_bitmap_within_rect\n"); } @@ -114,10 +130,10 @@ unsigned long sys_bagl_hal_draw_bitmap_within_rect( len = build_chunk(buf + size, &offset, sizeof(buf) - size, bitmap, bitmap_length); - header[0] = SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS; + header[0] = SEPROXYHAL_TAG_BAGL_DRAW_BITMAP; header[1] = ((size + len + 1) >> 8) & 0xff; header[2] = (size + len + 1) & 0xff; - header[3] = SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START; + header[3] = SEPROXYHAL_TAG_BAGL_DRAW_BITMAP_START; sys_io_seph_send(header, sizeof(header)); sys_io_seph_send(buf, size + len); @@ -132,10 +148,10 @@ unsigned long sys_bagl_hal_draw_bitmap_within_rect( uint8_t tmp[64]; sys_io_seproxyhal_spi_recv(tmp, sizeof(tmp), 0); - header[0] = SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS; + header[0] = SEPROXYHAL_TAG_BAGL_DRAW_BITMAP; header[1] = ((len + 1) >> 8) & 0xff; header[2] = (len + 1) & 0xff; - header[3] = SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_CONT; + header[3] = SEPROXYHAL_TAG_BAGL_DRAW_BITMAP_CONT; sys_io_seph_send(header, sizeof(header)); sys_io_seph_send(buf, len); diff --git a/src/bolos/cx.c b/src/bolos/cx.c index ddeaad4b..0a7fc990 100644 --- a/src/bolos/cx.c +++ b/src/bolos/cx.c @@ -4,32 +4,20 @@ #include #include #include -#include #include #include "emulate.h" +#include "environment.h" static bool initialized = false; -static unsigned int get_rng_seed_from_env(const char *name) -{ - char *p; - - p = getenv(name); - if (p != NULL) { - return atoi(p); - } else { - return time(NULL); - } -} - /* not secure, but this is clearly not the goal of this emulator */ unsigned long sys_cx_rng(uint8_t *buffer, unsigned int length) { unsigned int i; if (!initialized) { - srand(get_rng_seed_from_env("RNG_SEED")); + srand(env_get_rng()); initialized = true; } diff --git a/src/bolos/cx_aes_sdk2.c b/src/bolos/cx_aes_sdk2.c index 1a43fb36..a49bb073 100644 --- a/src/bolos/cx_aes_sdk2.c +++ b/src/bolos/cx_aes_sdk2.c @@ -1,51 +1,107 @@ #define _SDK_2_0_ #include +#include #include #include #include +#include "bolos/cx_utils.h" #include "bolos/cxlib.h" //----------------------------------------------------------------------------- -static cx_aes_key_t local_aes_key; +static AES_KEY local_aes_key; static bool local_aes_ready; -static uint32_t local_aes_mode; +static uint32_t local_aes_op; +static uint32_t local_aes_chain_mode; +bool hdw_cbc = false; +bool set_aes_iv; +static uint8_t aes_current_block[AES_BLOCK_SIZE] = { 0 }; //----------------------------------------------------------------------------- // AES related functions: //----------------------------------------------------------------------------- cx_err_t sys_cx_aes_set_key_hw(const cx_aes_key_t *key, uint32_t mode) { - memcpy(&local_aes_key, key, sizeof(local_aes_key)); - local_aes_mode = mode; + switch (mode & CX_MASK_SIGCRYPT) { + case CX_ENCRYPT: + case CX_SIGN: + case CX_VERIFY: + AES_set_encrypt_key(key->keys, (int)key->size * 8, &local_aes_key); + break; + case CX_DECRYPT: + AES_set_decrypt_key(key->keys, (int)key->size * 8, &local_aes_key); + break; + default: + local_aes_ready = false; + return CX_INVALID_PARAMETER; + } + local_aes_op = mode & CX_MASK_SIGCRYPT; + local_aes_chain_mode = mode & CX_MASK_CHAIN; + set_aes_iv = hdw_cbc; local_aes_ready = true; return CX_OK; } +// This function aims at reproducing a CBC mode performed in hardware +static cx_err_t cx_aes_block_hw_cbc(const unsigned char *inblock, + unsigned char *outblock) +{ + if (local_aes_op == CX_DECRYPT) { + uint8_t inblock_prev_value[AES_BLOCK_SIZE] = { 0 }; + // If the same buffer is used for inblock and outblock + // save inblock value for next block encryption + memcpy(inblock_prev_value, inblock, AES_BLOCK_SIZE); + + AES_decrypt(inblock, outblock, &local_aes_key); + // XOR the decryption result with aes_current_block + cx_memxor(outblock, aes_current_block, AES_BLOCK_SIZE); + + // Store the input block for next block decryption + memcpy(aes_current_block, inblock_prev_value, AES_BLOCK_SIZE); + } else { // CX_SIGN, CX_VERIFY, CX_ENCRYPT: + + // Before the encryption, XOR the input block with the + // previous value of aes_current_block which is either + // the IV or the previous ciphertext block + cx_memxor(aes_current_block, inblock, AES_BLOCK_SIZE); + + AES_encrypt(aes_current_block, outblock, &local_aes_key); + + // Store the ciphertext block for next block encryption + memcpy(aes_current_block, outblock, AES_BLOCK_SIZE); + } + + return CX_OK; +} + cx_err_t sys_cx_aes_block_hw(const unsigned char *inblock, unsigned char *outblock) { - AES_KEY aes_key; if (!local_aes_ready) { return CX_INTERNAL_ERROR; } - if ((local_aes_mode & CX_MASK_SIGCRYPT) == CX_DECRYPT) { - AES_set_decrypt_key(local_aes_key.keys, (int)local_aes_key.size * 8, - &aes_key); - AES_decrypt(inblock, outblock, &aes_key); + // Stores the IV + if (set_aes_iv && (local_aes_chain_mode == CX_CHAIN_CBC)) { + memcpy(aes_current_block, inblock, AES_BLOCK_SIZE); + set_aes_iv = false; + return CX_OK; + } + if (hdw_cbc && (local_aes_chain_mode == CX_CHAIN_CBC)) { + return cx_aes_block_hw_cbc(inblock, outblock); + } + if (local_aes_op == CX_DECRYPT) { + AES_decrypt(inblock, outblock, &local_aes_key); } else { // CX_SIGN, CX_VERIFY, CX_ENCRYPT: - AES_set_encrypt_key(local_aes_key.keys, (int)local_aes_key.size * 8, - &aes_key); - AES_encrypt(inblock, outblock, &aes_key); + AES_encrypt(inblock, outblock, &local_aes_key); } - OPENSSL_cleanse(&aes_key, sizeof(aes_key)); return CX_OK; } void sys_cx_aes_reset_hw(void) { + OPENSSL_cleanse(&local_aes_key, sizeof(local_aes_key)); local_aes_ready = false; } diff --git a/src/bolos/cx_bn.c b/src/bolos/cx_bn.c index 63d93a9a..99205844 100644 --- a/src/bolos/cx_bn.c +++ b/src/bolos/cx_bn.c @@ -481,3 +481,21 @@ cx_err_t sys_cx_bn_next_prime(const cx_bn_t bn_x) end: return error; } + +cx_err_t sys_cx_bn_gf2_n_mul(cx_bn_t bn_r, const cx_bn_t bn_a, + const cx_bn_t bn_b, const cx_bn_t bn_n, + const cx_bn_t bn_h) +{ + cx_err_t error = CX_OK; // By default, until some error occurs + cx_mpi_t *r, *a, *b, *n, *h; + + // Convert bn to mpi + CX_CHECK(cx_bn_rab_to_mpi(bn_r, &r, bn_a, &a, bn_b, &b)); + CX_CHECK(cx_bn_ab_to_mpi(bn_n, &n, bn_h, &h)); + + // Perform a Galois field multiplication operation reduced by n + CX_CHECK(cx_mpi_gf2_n_mul(r, a, b, n, h)); + +end: + return error; +} diff --git a/src/bolos/cx_crc.c b/src/bolos/cx_crc.c index 53ab49f0..337ab5cc 100644 --- a/src/bolos/cx_crc.c +++ b/src/bolos/cx_crc.c @@ -4,7 +4,7 @@ #define cx_crc16_update sys_cx_crc16_update -static unsigned short const cx_ccitt16[] = { +static const unsigned short cx_ccitt16[] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, @@ -50,7 +50,7 @@ unsigned short sys_cx_crc16_update(unsigned short crc, const void *buf, #define cx_crc32_update sys_cx_crc32_update -static unsigned int cx_ccitt32[] = { +static const unsigned int cx_ccitt32[] = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, diff --git a/src/bolos/cx_ec.c b/src/bolos/cx_ec.c index 81e7afb1..0326a8dc 100644 --- a/src/bolos/cx_ec.c +++ b/src/bolos/cx_ec.c @@ -1652,7 +1652,6 @@ int sys_cx_ecdsa_sign(const cx_ecfp_private_key_t *key, int mode, BIGNUM *normalized_s = BN_new(); ECDSA_SIG_get0(ecdsa_sig, &r, &s); if ((mode & CX_NO_CANONICAL) == 0 && BN_cmp(s, halfn) > 0) { - fprintf(stderr, "cx_ecdsa_sign: normalizing s > n/2\n"); BN_sub(normalized_s, n, s); if (info != NULL) { *info ^= CX_ECCINFO_PARITY_ODD; // Inverse the bit diff --git a/src/bolos/cx_hmac.c b/src/bolos/cx_hmac.c index 5f59176c..62c6f47e 100644 --- a/src/bolos/cx_hmac.c +++ b/src/bolos/cx_hmac.c @@ -2,9 +2,8 @@ #include #include -#include "cx.h" -//#include "cx_utils.h" #include "bolos/exception.h" +#include "cx.h" #define IPAD 0x36u #define OPAD 0x5cu diff --git a/src/bolos/cx_mpi.c b/src/bolos/cx_mpi.c index dbec1376..cf600af3 100644 --- a/src/bolos/cx_mpi.c +++ b/src/bolos/cx_mpi.c @@ -1167,6 +1167,19 @@ cx_err_t cx_mpi_next_prime(cx_mpi_t *x) return error; } +cx_err_t cx_mpi_gf2_n_mul(cx_mpi_t *r, const cx_mpi_t *a, const cx_mpi_t *b, + const cx_mpi_t *n, + const cx_mpi_t *h __attribute__((unused))) +{ + cx_err_t error = CX_OK; // By default, until some error occurs + + if (!BN_GF2m_mod_mul(r, a, b, n, local_bn_ctx)) { + error = CX_INTERNAL_ERROR; + } + + return error; +} + #define MAX_BYTE_LEN 128 void cx_mpi_reverse(cx_mpi_t *x, uint32_t nbytes) { diff --git a/src/bolos/cx_utils.h b/src/bolos/cx_utils.h index 05e43e05..c8e3b629 100644 --- a/src/bolos/cx_utils.h +++ b/src/bolos/cx_utils.h @@ -10,7 +10,7 @@ #ifndef NATIVE_64BITS // NO 64BITS /** 64bits types, native or by-hands, depending on target and/or compiler * support. - * This type is defined here only because sha-3 struct used it INTENALLY. + * This type is defined here only because sha-3 struct used it INTERNALLY. * It should never be directly used by other modules. */ struct uint64_s { diff --git a/src/bolos/cxlib.h b/src/bolos/cxlib.h index 59fe2144..747d2272 100644 --- a/src/bolos/cxlib.h +++ b/src/bolos/cxlib.h @@ -134,6 +134,8 @@ cx_err_t cx_mpi_mod_pow(cx_mpi_t *r, const cx_mpi_t *a, const cx_mpi_t *e, const cx_mpi_t *n); cx_err_t cx_mpi_is_prime(cx_mpi_t *x, bool *prime); cx_err_t cx_mpi_next_prime(cx_mpi_t *x); +cx_err_t cx_mpi_gf2_n_mul(cx_mpi_t *r, const cx_mpi_t *a, const cx_mpi_t *b, + const cx_mpi_t *n, const cx_mpi_t *h); void cx_mpi_reverse(cx_mpi_t *x, uint32_t nbytes); void cx_mpi_swap(cx_mpi_t *a, cx_mpi_t *b, const int c); @@ -196,6 +198,9 @@ cx_err_t sys_cx_bn_mod_pow2(cx_bn_t bn_r, const cx_bn_t bn_a, const uint8_t *e, uint32_t len_e, const cx_bn_t bn_n); cx_err_t sys_cx_bn_is_prime(const cx_bn_t bn_x, bool *prime); cx_err_t sys_cx_bn_next_prime(const cx_bn_t bn_x); +cx_err_t sys_cx_bn_gf2_n_mul(cx_bn_t bn_r, const cx_bn_t bn_a, + const cx_bn_t bn_b, const cx_bn_t bn_n, + const cx_bn_t bn_h); // cx_ecdomain.c int cx_nid_from_curve(cx_curve_t curve); diff --git a/src/bolos/endorsement.c b/src/bolos/endorsement.c index 27141601..41fd1017 100644 --- a/src/bolos/endorsement.c +++ b/src/bolos/endorsement.c @@ -3,51 +3,10 @@ #include "bolos/exception.h" #include "cx.h" #include "emulate.h" +#include "environment.h" #define cx_ecdsa_init_public_key sys_cx_ecfp_init_public_key -// TODO: all keys are currently hardcoded - -static cx_ecfp_private_key_t user_private_key_1 = { - CX_CURVE_256K1, - 32, - { 0xe1, 0x5e, 0x01, 0xd4, 0x70, 0x82, 0xf0, 0xea, 0x47, 0x71, 0xc9, - 0x9f, 0xe3, 0x12, 0xf9, 0xd7, 0x00, 0x93, 0xc8, 0x9a, 0xf4, 0x77, - 0x87, 0xfd, 0xf8, 0x2e, 0x03, 0x1f, 0x67, 0x28, 0xb7, 0x10 }, -}; - -static cx_ecfp_private_key_t user_private_key_2 = { - CX_CURVE_256K1, - 32, - { 0xe1, 0x5e, 0x01, 0xd4, 0x70, 0x82, 0xf0, 0xea, 0x47, 0x71, 0xc9, - 0x9f, 0xe3, 0x12, 0xf9, 0xd7, 0x00, 0x93, 0xc8, 0x9a, 0xf4, 0x77, - 0x87, 0xfd, 0xf8, 0x2e, 0x03, 0x1f, 0x67, 0x28, 0xb7, 0x10 }, -}; - -// user_private_key_1 signed by test owner private key -// "138fb9b91da745f12977a2b46f0bce2f0418b50fcb76631baf0f08ceefdb5d57" -static uint8_t user_certificate_1[] = { - 0x30, 0x45, 0x02, 0x21, 0x00, 0xbf, 0x23, 0x7e, 0x5b, 0x40, 0x06, 0x14, - 0x17, 0xf6, 0x62, 0xa6, 0xd0, 0x8a, 0x4b, 0xde, 0x1f, 0xe3, 0x34, 0x3b, - 0xd8, 0x70, 0x8c, 0xed, 0x04, 0x6c, 0x84, 0x17, 0x49, 0x5a, 0xd3, 0x6c, - 0xcf, 0x02, 0x20, 0x3d, 0x39, 0xa5, 0x32, 0xee, 0xca, 0xdf, 0xf6, 0xdf, - 0x20, 0x53, 0xe4, 0xab, 0x98, 0x96, 0xaa, 0x00, 0xf3, 0xbe, 0xf1, 0x5c, - 0x4b, 0xd1, 0x1c, 0x53, 0x66, 0x1e, 0x54, 0xfe, 0x5e, 0x2f, 0xf4 -}; -static const uint8_t user_certificate_1_length = sizeof(user_certificate_1); - -// user_private_key_2 signed by test owner private key -// "138fb9b91da745f12977a2b46f0bce2f0418b50fcb76631baf0f08ceefdb5d57" -static uint8_t user_certificate_2[] = { - 0x30, 0x45, 0x02, 0x21, 0x00, 0xbf, 0x23, 0x7e, 0x5b, 0x40, 0x06, 0x14, - 0x17, 0xf6, 0x62, 0xa6, 0xd0, 0x8a, 0x4b, 0xde, 0x1f, 0xe3, 0x34, 0x3b, - 0xd8, 0x70, 0x8c, 0xed, 0x04, 0x6c, 0x84, 0x17, 0x49, 0x5a, 0xd3, 0x6c, - 0xcf, 0x02, 0x20, 0x3d, 0x39, 0xa5, 0x32, 0xee, 0xca, 0xdf, 0xf6, 0xdf, - 0x20, 0x53, 0xe4, 0xab, 0x98, 0x96, 0xaa, 0x00, 0xf3, 0xbe, 0xf1, 0x5c, - 0x4b, 0xd1, 0x1c, 0x53, 0x66, 0x1e, 0x54, 0xfe, 0x5e, 0x2f, 0xf4 -}; -static uint8_t user_certificate_2_length; - unsigned int sys_os_endorsement_get_code_hash(uint8_t *buffer) { memcpy(buffer, "12345678abcdef0000fedcba87654321", 32); @@ -61,10 +20,10 @@ unsigned long sys_os_endorsement_get_public_key(uint8_t index, uint8_t *buffer) switch (index) { case 1: - privateKey = &user_private_key_1; + privateKey = env_get_user_private_key(1); break; case 2: - privateKey = &user_private_key_2; + privateKey = env_get_user_private_key(2); break; default: THROW(EXCEPTION); @@ -93,30 +52,27 @@ unsigned int sys_os_endorsement_get_public_key_certificate(unsigned char index, unsigned char *buffer) { - unsigned char *certificate; - unsigned char length; + env_user_certificate_t *certificate; switch (index) { case 1: - length = user_certificate_1_length; - certificate = user_certificate_1; + certificate = env_get_user_certificate(1); break; case 2: - length = user_certificate_2_length; - certificate = user_certificate_2; + certificate = env_get_user_certificate(2); break; default: THROW(EXCEPTION); break; } - if (length == 0) { + if (certificate->length == 0) { THROW(EXCEPTION); } - memcpy(buffer, certificate, length); + memcpy(buffer, certificate->buffer, certificate->length); - return length; + return certificate->length; } unsigned int sys_os_endorsement_get_public_key_certificate_new( @@ -140,7 +96,8 @@ unsigned long sys_os_endorsement_key1_sign_data(uint8_t *data, sys_cx_hash((cx_hash_t *)&sha256, CX_LAST, hash, sizeof(hash), hash, 32); /* XXX: CX_RND_TRNG is set but actually ignored by speculos' * sys_cx_ecdsa_sign implementation */ - sys_cx_ecdsa_sign(&user_private_key_1, CX_LAST | CX_RND_TRNG, CX_SHA256, hash, + sys_cx_ecdsa_sign(env_get_user_private_key(1), CX_LAST | CX_RND_TRNG, + CX_SHA256, hash, sizeof(hash), // size of SHA256 hash signature, 6 + 33 * 2, /*3TL+2V*/ NULL); diff --git a/src/bolos/fonts_info.c b/src/bolos/fonts_info.c new file mode 100644 index 00000000..4783fc80 --- /dev/null +++ b/src/bolos/fonts_info.c @@ -0,0 +1,315 @@ +#include +#include +#include + +#include "fonts.h" +#include "sdk.h" + +#define MAX_BITMAP_CHAR (MAX_NB_FONTS * 128) +#define MAX_BITMAP_CHAR_12 (MAX_NB_FONTS_12 * 128) + +typedef struct { + const uint8_t *bitmap; + uint32_t character; +} BITMAP_CHAR; + +BITMAP_CHAR bitmap_char[MAX_BITMAP_CHAR_12]; +uint32_t nb_bitmap_char; + +// Return the real addr depending on where the app was loaded +static void *remap_addr(void *code, uint32_t addr, uint32_t text_load_addr) +{ + // No remap on Stax, fonts are loaded at a fixed location + if (hw_model == MODEL_STAX) { + return (void *)addr; + } + uint8_t *ptr = code; + ptr += addr - text_load_addr; + + return ptr; +} + +// Store bitmap/character pair +static void add_bitmap_character(uint8_t *bitmap, uint32_t character) +{ + if (sdk_version > SDK_API_LEVEL_14) { + if (nb_bitmap_char >= MAX_BITMAP_CHAR) { + fprintf(stdout, "ERROR: we reached MAX_BITMAP_CHAR!\n"); + return; + } + } else { + if (nb_bitmap_char >= MAX_BITMAP_CHAR_12) { + fprintf(stdout, "ERROR: we reached MAX_BITMAP_CHAR_12!\n"); + return; + } + } + // Space character is empty and have no bitmap -> erase it with next character + if (nb_bitmap_char && bitmap_char[nb_bitmap_char - 1].bitmap == bitmap) { + bitmap_char[nb_bitmap_char - 1].character = character; + return; + } + bitmap_char[nb_bitmap_char].bitmap = bitmap; + bitmap_char[nb_bitmap_char].character = character; + ++nb_bitmap_char; +} + +// Function used by qsort/bsearch to quickly sort/find bitmap_char pairs +int compare_bitmap_char(const void *ptr1, const void *ptr2) +{ + return ((const BITMAP_CHAR *)ptr1)->bitmap - + ((const BITMAP_CHAR *)ptr2)->bitmap; +} + +// Return the character corresponding to provided bitmap, or 0 if not found +uint32_t get_character_from_bitmap(const uint8_t *bitmap) +{ + BITMAP_CHAR value; + BITMAP_CHAR *result; + + if (!nb_bitmap_char) { + return 0; + } + + value.bitmap = bitmap; + value.character = 0; + + // Use bsearch to speed lookup + result = bsearch(&value, bitmap_char, nb_bitmap_char, sizeof(bitmap_char[0]), + compare_bitmap_char); + + if (result != NULL) { + return result->character; + } + return 0; +} + +// Parse provided NBGL font and add bitmap/character pairs +static void parse_nbgl_font(nbgl_font_t *nbgl_font) +{ + uint8_t *bitmap = nbgl_font->bitmap; + nbgl_font_character_t *characters = nbgl_font->characters; + + for (uint32_t c = nbgl_font->first_char; c <= nbgl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= nbgl_font->bitmap_len) { + fprintf(stdout, "bitmap_offset (%u) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, nbgl_font->bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse provided NBGL font and add bitmap/character pairs +static void parse_nbgl_font_14(nbgl_font_t_14 *nbgl_font) +{ + uint8_t *bitmap = nbgl_font->bitmap; + nbgl_font_character_t_14 *characters = nbgl_font->characters; + + for (uint32_t c = nbgl_font->first_char; c <= nbgl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= nbgl_font->bitmap_len) { + fprintf(stdout, "bitmap_offset (%d) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, nbgl_font->bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse provided NBGL font and add bitmap/character pairs +static void parse_nbgl_font_12(nbgl_font_t_12 *nbgl_font) +{ + uint8_t *bitmap = nbgl_font->bitmap; + nbgl_font_character_t_12 *characters = nbgl_font->characters; + + for (uint32_t c = nbgl_font->first_char; c <= nbgl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= nbgl_font->bitmap_len) { + fprintf(stdout, "bitmap_offset (%d) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, nbgl_font->bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse provided BAGL font and add bitmap/character pairs +static void parse_bagl_font(bagl_font_t *bagl_font, void *code, + unsigned long text_load_addr) +{ + uint8_t *bitmap = + remap_addr(code, (uint32_t)bagl_font->bitmap, text_load_addr); + bagl_font_character_t *characters = + remap_addr(code, (uint32_t)bagl_font->characters, text_load_addr); + + for (uint32_t c = bagl_font->first_char; c <= bagl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= bagl_font->bitmap_len) { + fprintf(stdout, "bitmap_offset (%d) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, bagl_font->bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse provided SDK_API_LEVEL_5 BAGL font and add bitmap/character pairs +static void parse_bagl_font_5(bagl_font_t_5 *bagl_font, void *code, + unsigned long text_load_addr) +{ + uint8_t *bitmap = + remap_addr(code, (uint32_t)bagl_font->bitmap, text_load_addr); + bagl_font_character_t_5 *characters = + remap_addr(code, (uint32_t)bagl_font->characters, text_load_addr); + uint32_t last_character = bagl_font->last_char - bagl_font->first_char; + uint32_t bitmap_len = characters[last_character].bitmap_offset + + characters[last_character].bitmap_byte_count; + + for (uint32_t c = bagl_font->first_char; c <= bagl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= bitmap_len) { + fprintf(stdout, "bitmap_offset (%d) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse provided SDK_API_LEVEL_1 BAGL font and add bitmap/character pairs +static void parse_bagl_font_1(bagl_font_t_1 *bagl_font, void *code, + unsigned long text_load_addr) +{ + uint8_t *bitmap = + remap_addr(code, (uint32_t)bagl_font->bitmap, text_load_addr); + bagl_font_character_t_1 *characters = + remap_addr(code, (uint32_t)bagl_font->characters, text_load_addr); + uint32_t last_character = bagl_font->last_char - bagl_font->first_char; + uint32_t bitmap_len = characters[last_character].bitmap_offset + + characters[last_character].bitmap_byte_count; + + for (uint32_t c = bagl_font->first_char; c <= bagl_font->last_char; + c++, characters++) { + // Be sure data is coherent + if (characters->bitmap_offset >= bitmap_len) { + fprintf(stdout, "bitmap_offset (%d) is >= bitmap_len (%u)!\n", + characters->bitmap_offset, bitmap_len); + return; + } + uint8_t *ptr = bitmap + characters->bitmap_offset; + add_bitmap_character(ptr, c); + } +} + +// Parse all fonts located at provided addr +void parse_fonts(void *code, unsigned long text_load_addr, + unsigned long fonts_addr, unsigned long fonts_size) +{ + // Number of fonts stored at fonts_addr + uint32_t nb_fonts; + uint32_t *fonts; + + nb_bitmap_char = 0; + + // Be sure API_LEVEL is supported + switch (sdk_version) { + // Supported API_LEVELs + case SDK_API_LEVEL_1: + case SDK_API_LEVEL_5: + case SDK_API_LEVEL_12: + case SDK_API_LEVEL_13: + case SDK_API_LEVEL_14: + case SDK_API_LEVEL_15: + break; + default: + // Unsupported API_LEVEL, will not parse fonts! + return; + } + // On Stax, fonts are loaded at a known location + if (hw_model == MODEL_STAX) { + fonts = (void *)STAX_FONTS_ARRAY_ADDR; + if (sdk_version > SDK_API_LEVEL_14) { + nb_fonts = STAX_NB_FONTS; + } else { + nb_fonts = STAX_NB_FONTS_12; + } + } else { + fonts = remap_addr(code, fonts_addr, text_load_addr); + nb_fonts = fonts_size / 4; + } + + // There is no font or we don't know its format + if (!nb_fonts) { + return; + } + + // Checks that fonts & nb_fonts are coherent + if (fonts[nb_fonts] != nb_fonts) { + fprintf(stdout, "ERROR: Expecting nb_fonts=%u and found %u instead!\n", + nb_fonts, fonts[nb_fonts]); + return; + } + if (sdk_version > SDK_API_LEVEL_14) { + if (nb_fonts > MAX_NB_FONTS) { + fprintf(stdout, + "ERROR: nb_fonts (%u) is bigger than MAX_NB_FONTS (%d)!\n", + nb_fonts, MAX_NB_FONTS); + return; + } + } else { + if (nb_fonts > MAX_NB_FONTS_12) { + fprintf(stdout, + "ERROR: nb_fonts (%u) is bigger than MAX_NB_FONTS_12 (%d)!\n", + nb_fonts, MAX_NB_FONTS_12); + return; + } + } + + // Parse all those fonts and add bitmap/character pairs + for (uint32_t i = 0; i < nb_fonts; i++) { + if (hw_model == MODEL_STAX) { + switch (sdk_version) { + case SDK_API_LEVEL_12: + case SDK_API_LEVEL_13: + parse_nbgl_font_12((void *)fonts[i]); + break; + case SDK_API_LEVEL_14: + parse_nbgl_font_14((void *)fonts[i]); + break; + default: + parse_nbgl_font((void *)fonts[i]); + break; + } + } else { + void *font = remap_addr(code, fonts[i], text_load_addr); + + switch (sdk_version) { + case SDK_API_LEVEL_1: + parse_bagl_font_1(font, code, text_load_addr); + break; + case SDK_API_LEVEL_5: + parse_bagl_font_5(font, code, text_load_addr); + break; + default: + parse_bagl_font(font, code, text_load_addr); + break; + } + } + } + + // Sort all those bitmap/character pairs (should be quick, they are almost + // sorted) + qsort(bitmap_char, nb_bitmap_char, sizeof(bitmap_char[0]), + compare_bitmap_char); +} diff --git a/src/bolos/nbgl.c b/src/bolos/nbgl.c index de40f2d4..fbf9b92c 100644 --- a/src/bolos/nbgl.c +++ b/src/bolos/nbgl.c @@ -3,14 +3,16 @@ #include #include "emulate.h" +#include "fonts.h" #include "nbgl.h" #include "nbgl_rle.h" -#define SEPROXYHAL_TAG_NBGL_DRAW_RECT 0x6A -#define SEPROXYHAL_TAG_NBGL_REFRESH 0x6B -#define SEPROXYHAL_TAG_NBGL_DRAW_LINE 0x6C -#define SEPROXYHAL_TAG_NBGL_DRAW_IMAGE 0x6D -#define SEPROXYHAL_TAG_NBGL_DRAW_IMAGE_FILE 0x6E +#define SEPROXYHAL_TAG_NBGL_DRAW_RECT 0xFA +#define SEPROXYHAL_TAG_NBGL_REFRESH 0xFB +#define SEPROXYHAL_TAG_NBGL_DRAW_LINE 0xFC +#define SEPROXYHAL_TAG_NBGL_DRAW_IMAGE 0xFD +#define SEPROXYHAL_TAG_NBGL_DRAW_IMAGE_FILE 0xFE +#define SEPROXYHAL_TAG_NBGL_DRAW_IMAGE_RLE 0xFF unsigned long sys_nbgl_front_draw_rect(nbgl_area_t *area) { @@ -46,15 +48,16 @@ unsigned long sys_nbgl_front_draw_horizontal_line(nbgl_area_t *area, return 0; } -unsigned long sys_nbgl_front_draw_img(nbgl_area_t *area, uint8_t *buffer, - nbgl_transformation_t transformation, - nbgl_color_map_t colorMap) +static unsigned long +nbgl_front_draw_img_character(nbgl_area_t *area, uint8_t *buffer, + nbgl_transformation_t transformation, + nbgl_color_map_t colorMap, uint32_t character) { uint8_t header[3]; uint8_t bpp = 1 << area->bpp; uint32_t nb_pixs = (area->width * area->height * bpp); uint32_t buffer_len = (nb_pixs / 8) + ((nb_pixs % 8) > 0); - size_t len = sizeof(nbgl_area_t) + buffer_len + 2; + size_t len = sizeof(nbgl_area_t) + buffer_len + 1 + 1 + 4; header[0] = SEPROXYHAL_TAG_NBGL_DRAW_IMAGE; header[1] = (len >> 8) & 0xff; @@ -65,10 +68,21 @@ unsigned long sys_nbgl_front_draw_img(nbgl_area_t *area, uint8_t *buffer, sys_io_seph_send(buffer, buffer_len); sys_io_seph_send((const uint8_t *)&transformation, 1); sys_io_seph_send((const uint8_t *)&colorMap, 1); + sys_io_seph_send((const uint8_t *)&character, 4); return 0; } +unsigned long sys_nbgl_front_draw_img(nbgl_area_t *area, uint8_t *buffer, + nbgl_transformation_t transformation, + nbgl_color_map_t colorMap) +{ + // Try to find the character corresponding to provided bitmap + uint32_t character = get_character_from_bitmap(buffer); + return nbgl_front_draw_img_character(area, buffer, transformation, colorMap, + character); +} + unsigned long sys_nbgl_front_refresh_area_legacy(nbgl_area_t *area) { uint8_t header[3]; @@ -98,17 +112,24 @@ unsigned long sys_nbgl_front_draw_img_file(nbgl_area_t *area, uint8_t *buffer, uint8_t header[3]; uint8_t compressed = buffer[4] & 0xF; - if (compressed && optional_uzlib_work_buffer == NULL) { + if (compressed == 1 && optional_uzlib_work_buffer == NULL) { fprintf(stderr, "no uzlib work buffer provided, failing"); return 0; } - size_t len = sizeof(nbgl_area_t) + 1; - size_t buffer_len; - if (compressed) { - buffer_len = (buffer[5] | (buffer[5 + 1] << 8) | (buffer[5 + 2] << 16)) + 8; - } else { + size_t buffer_len = 0; + switch (compressed) { + case 0: // no compression buffer_len = (area->width * area->height * (area->bpp + 1)) / 8; + break; + case 1: // gzlib compression + buffer_len = (buffer[5] | (buffer[5 + 1] << 8) | (buffer[5 + 2] << 16)) + 8; + break; + case 2: // rle compression + buffer_len = (buffer[5] | (buffer[5 + 1] << 8) | (buffer[5 + 2] << 16)); + buffer += 8; + return sys_nbgl_front_draw_img_rle(area, buffer, buffer_len, + ((colorMap >> (0 * 2)) & 0x3), 0); } len += buffer_len; @@ -125,14 +146,12 @@ unsigned long sys_nbgl_front_draw_img_file(nbgl_area_t *area, uint8_t *buffer, return 0; } -#define FONTS_ARRAY_ADDR 0x00805000 -#define NB_FONTS 7 unsigned long sys_nbgl_get_font(unsigned int fontId) { - if (fontId >= NB_FONTS) { + if (fontId >= STAX_NB_FONTS) { return 0; } else { - return *((unsigned int *)(FONTS_ARRAY_ADDR + (4 * fontId))); + return *((unsigned int *)(STAX_FONTS_ARRAY_ADDR + (4 * fontId))); } } @@ -143,24 +162,53 @@ unsigned long sys_nbgl_screen_reinit(void) uint8_t uncompress_rle_buffer[SCREEN_HEIGHT * SCREEN_WIDTH / 2]; -unsigned long sys_nbgl_front_draw_img_rle(nbgl_area_t *area, uint8_t *buffer, - uint32_t buffer_len, - color_t fore_color) +unsigned long sys_nbgl_front_draw_img_rle_legacy(nbgl_area_t *area, + uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color) { + // Try to find the character corresponding to provided bitmap + uint32_t character = get_character_from_bitmap(buffer); + // Uncompress input buffer nbgl_uncompress_rle(area, buffer, buffer_len, uncompress_rle_buffer, sizeof(uncompress_rle_buffer)); // Now send it as if it was an uncompressed image - sys_nbgl_front_draw_img(area, uncompress_rle_buffer, NO_TRANSFORMATION, - fore_color); + nbgl_front_draw_img_character(area, uncompress_rle_buffer, NO_TRANSFORMATION, + fore_color, character); + return 0; } -unsigned long sys_nbgl_front_draw_img_rle_legacy(nbgl_area_t *area, - uint8_t *buffer, - uint32_t buffer_len, - color_t fore_color) +unsigned long sys_nbgl_front_draw_img_rle_10(nbgl_area_t *area, uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color) +{ + return sys_nbgl_front_draw_img_rle(area, buffer, buffer_len, fore_color, 0); +} + +unsigned long sys_nbgl_front_draw_img_rle(nbgl_area_t *area, uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color, + uint8_t nb_skipped_bytes) { - return sys_nbgl_front_draw_img_rle(area, buffer, buffer_len, fore_color); -} \ No newline at end of file + // Try to find the character corresponding to provided bitmap + uint32_t character = get_character_from_bitmap(buffer); + // We need to keep data compressed to be able to compare with fonts bitmaps + uint8_t header[3]; + size_t len = sizeof(nbgl_area_t) + buffer_len + 1 + 1 + 4; + + header[0] = SEPROXYHAL_TAG_NBGL_DRAW_IMAGE_RLE; + header[1] = (len >> 8) & 0xff; + header[2] = len & 0xff; + + sys_io_seph_send(header, sizeof(header)); + sys_io_seph_send((const uint8_t *)area, sizeof(nbgl_area_t)); + sys_io_seph_send(buffer, buffer_len); + sys_io_seph_send((const uint8_t *)&fore_color, 1); + sys_io_seph_send((const uint8_t *)&nb_skipped_bytes, 1); + sys_io_seph_send((const uint8_t *)&character, 4); + + return 0; +} diff --git a/src/bolos/nbgl.h b/src/bolos/nbgl.h index fe59450b..c8083af7 100644 --- a/src/bolos/nbgl.h +++ b/src/bolos/nbgl.h @@ -126,11 +126,16 @@ unsigned long sys_nbgl_get_font(unsigned int fontId); unsigned long sys_nbgl_screen_reinit(void); -unsigned long sys_nbgl_front_draw_img_rle(nbgl_area_t *area, uint8_t *buffer, - uint32_t buffer_len, - color_t fore_color); +unsigned long sys_nbgl_front_draw_img_rle_10(nbgl_area_t *area, uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color); unsigned long sys_nbgl_front_draw_img_rle_legacy(nbgl_area_t *area, uint8_t *buffer, uint32_t buffer_len, - color_t fore_color); \ No newline at end of file + color_t fore_color); + +unsigned long sys_nbgl_front_draw_img_rle(nbgl_area_t *area, uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color, + uint8_t nb_skipped_bytes); diff --git a/src/bolos/nbgl_rle.c b/src/bolos/nbgl_rle.c index 38752085..4a7c7ab2 100644 --- a/src/bolos/nbgl_rle.c +++ b/src/bolos/nbgl_rle.c @@ -130,7 +130,10 @@ static void nbgl_uncompress_rle_4bpp(nbgl_area_t *area, uint8_t *buffer, return; } - memset(out_buffer, 0, out_buffer_len); + memset(out_buffer, 0xFF, out_buffer_len); + if (!buffer_len) { + return; + } uint32_t pix_cnt = 0; uint32_t read_cnt = 0; @@ -188,6 +191,9 @@ static void nbgl_uncompress_rle_1bpp(nbgl_area_t *area, uint8_t *buffer, size_t remaining_pixels = area->width * area->height; memset(out_buffer, 0, out_buffer_len); + if (!buffer_len) { + return; + } while (remaining_pixels > 0 && (index < buffer_len || nb_zeros || nb_ones)) { diff --git a/src/bolos/os.c b/src/bolos/os.c index c6319eb2..53ee675e 100644 --- a/src/bolos/os.c +++ b/src/bolos/os.c @@ -3,15 +3,13 @@ #include #include "emulate.h" +#include "environment.h" #include "svc.h" #define OS_SETTING_PLANEMODE_OLD 5 #define OS_SETTING_PLANEMODE_NEW 6 #define OS_SETTING_SOUND 9 -#define BOLOS_TAG_APPNAME 0x01 -#define BOLOS_TAG_APPVERSION 0x02 - #undef PATH_MAX #define PATH_MAX 1024 @@ -69,56 +67,7 @@ unsigned long sys_os_registry_get_current_app_tag(unsigned int tag, uint8_t *buffer, size_t length) { - const char *name; - const char *version; - const char *str; - char *str_dup = NULL; - - if (length < 1) { - return 0; - } - - name = "app"; - version = "1.33.7"; - - str = getenv("SPECULOS_APPNAME"); - if (str == NULL) { - str = getenv("SPECULOS_DETECTED_APPNAME"); - } - - if (str != NULL) { - str_dup = strdup(str); - if (str_dup != NULL) { - char *p = strstr(str_dup, ":"); - if (p != NULL) { - *p = '\x00'; - name = str_dup; - version = p + 1; - } - } - } - - switch (tag) { - case BOLOS_TAG_APPNAME: - strncpy((char *)buffer, name, length); - length = MIN(length, strlen(name)); - break; - case BOLOS_TAG_APPVERSION: - strncpy((char *)buffer, version, length); - length = MIN(length, strlen(version)); - break; - default: - length = 0; - break; - } - - buffer[length] = '\x00'; - - if (str_dup != NULL) { - free(str_dup); - } - - return length; + return env_get_app_tag((char *)buffer, length, tag); } unsigned long sys_os_lib_call(unsigned long *call_parameters) diff --git a/src/bolos/os_bip32.c b/src/bolos/os_bip32.c index 52610cad..161d331f 100644 --- a/src/bolos/os_bip32.c +++ b/src/bolos/os_bip32.c @@ -9,24 +9,15 @@ #include "cx.h" #include "cx_utils.h" #include "emulate.h" +#include "environment.h" #define BIP32_HARDEN_MASK 0x80000000 #define BIP32_SECP_SEED_LENGTH 12 -#define MAX_SEED_SIZE 64 #define cx_ecfp_generate_pair sys_cx_ecfp_generate_pair #define cx_ecfp_init_private_key sys_cx_ecfp_init_private_key #define cx_ecdsa_init_private_key cx_ecfp_init_private_key -/* glory promote mansion idle axis finger extra february uncover one trip - * resource lawn turtle enact monster seven myth punch hobby comfort wild raise - * skin */ -static uint8_t default_seed[MAX_SEED_SIZE] = - "\xb1\x19\x97\xfa\xff\x42\x0a\x33\x1b\xb4\xa4\xff\xdc\x8b\xdc\x8b\xa7\xc0" - "\x17\x32\xa9\x9a\x30\xd8\x3d\xbb\xeb\xd4\x69\x66\x6c\x84\xb4\x7d\x09\xd3" - "\xf5\xf4\x72\xb3\xb9\x38\x4a\xc6\x34\xbe\xba\x2a\x44\x0b\xa3\x6e\xc7\x66" - "\x11\x44\x13\x2f\x35\xe2\x06\x87\x35\x64"; - static uint8_t const BIP32_SECP_SEED[] = { 'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd' }; @@ -166,65 +157,6 @@ static void expand_seed(cx_curve_t curve, const uint8_t *sk, size_t sk_length, } } -int unhex(uint8_t *dst, size_t dst_size, const char *src, size_t src_size) -{ - unsigned int i; - uint8_t acc; - int8_t c; - - acc = 0; - for (i = 0; i < src_size && (i >> 1) < dst_size; i++) { - c = src[i]; - switch (c) { - case '0' ... '9': - acc = (acc << 4) + c - '0'; - break; - case 'a' ... 'f': - acc = (acc << 4) + c - 'a' + 10; - break; - case 'A' ... 'F': - acc = (acc << 4) + c - 'A' + 10; - break; - default: - return -1; - } - - if (i % 2 != 0) { - dst[i >> 1] = acc; - acc = 0; - } - } - - if (i != src_size) { - return -1; - } - - return src_size / 2; -} - -size_t get_seed_from_env(const char *name, uint8_t *seed, size_t max_size) -{ - ssize_t seed_size; - char *p; - - p = getenv(name); - if (p != NULL) { - seed_size = unhex(seed, max_size, p, strlen(p)); - if (seed_size < 0) { - warnx("invalid seed passed through %s environment variable", name); - p = NULL; - } - } - - if (p == NULL) { - warnx("using default seed"); - memcpy(seed, default_seed, sizeof(default_seed)); - seed_size = sizeof(default_seed); - } - - return seed_size; -} - static int hdw_bip32_ed25519(extended_private_key *key, const uint32_t *path, size_t length, uint8_t *private_key, uint8_t *chain) @@ -478,6 +410,10 @@ unsigned long sys_os_perso_derive_node_with_seed_key( const uint8_t *sk; int ret; + if (path == NULL) { + THROW(EXCEPTION); + } + // In SDK2, some curves don't have the same value: switch ((int)curve) { case 0x71: @@ -508,7 +444,7 @@ unsigned long sys_os_perso_derive_node_with_seed_key( sk_length = seed_key_length; } - seed_size = get_seed_from_env("SPECULOS_SEED", seed, sizeof(seed)); + seed_size = env_get_seed(seed, sizeof(seed)); if (mode == HDW_SLIP21) { ret = hdw_slip21(sk, sk_length, seed, seed_size, (const uint8_t *)path, diff --git a/src/bolos/os_bip32.h b/src/bolos/os_bip32.h index 57e0629d..743cddea 100644 --- a/src/bolos/os_bip32.h +++ b/src/bolos/os_bip32.h @@ -16,5 +16,3 @@ typedef struct { void expand_seed_bip32(const cx_curve_domain_t *domain, uint8_t *seed, unsigned int seed_length, extended_private_key *key); -int unhex(uint8_t *dst, size_t dst_size, const char *src, size_t src_size); -size_t get_seed_from_env(const char *name, uint8_t *seed, size_t max_size); diff --git a/src/bolos/os_eip2333.c b/src/bolos/os_eip2333.c index a753a35c..2e8e050c 100644 --- a/src/bolos/os_eip2333.c +++ b/src/bolos/os_eip2333.c @@ -6,8 +6,8 @@ #include "cx.h" #include "cx_utils.h" #include "emulate.h" +#include "environment.h" #include "exception.h" -#include "os_bip32.h" #define MAX_SEED_SIZE 64 #define CX_SHA256_SIZE 32 @@ -142,7 +142,7 @@ unsigned long sys_os_perso_derive_eip2333(cx_curve_t curve, THROW(EXCEPTION); } - seed_size = get_seed_from_env("SPECULOS_SEED", seed, sizeof(seed)); + seed_size = env_get_seed(seed, sizeof(seed)); cx_derive_master_sk(seed, seed_size, sk); if (privateKey != NULL) { diff --git a/src/bolos/seproxyhal.c b/src/bolos/seproxyhal.c index 65e92f74..c4d510be 100644 --- a/src/bolos/seproxyhal.c +++ b/src/bolos/seproxyhal.c @@ -7,9 +7,12 @@ #include "bolos/touch.h" #include "emulate.h" -#define SEPROXYHAL_TAG_STATUS_MASK 0x60 +// Only consider 0x6X tags as status one +#define SEPROXYHAL_TAG_STATUS_MASK 0xF0 +#define SEPROXYHAL_TAG_GENERAL_STATUS 0x60 -static bool status_sent = false; +static bool tx_status = false; +static uint32_t rx_length = 0; static uint8_t last_tag; static size_t next_length; @@ -67,7 +70,7 @@ static ssize_t writeall(int fd, const void *buf, size_t count) unsigned long sys_io_seproxyhal_spi_is_status_sent(void) { - return (unsigned long)status_sent; + return (unsigned long)tx_status; } unsigned long sys_io_seph_is_status_sent(void) @@ -85,6 +88,13 @@ unsigned long sys_io_seproxyhal_spi_send(const uint8_t *buffer, uint16_t length) if (length < 3) { THROW(INVALID_PARAMETER); } + + if (sys_io_seproxyhal_spi_is_status_sent() && buffer && + ((buffer[0] & SEPROXYHAL_TAG_STATUS_MASK) == + SEPROXYHAL_TAG_GENERAL_STATUS)) { + return 0; + } + last_tag = buffer[0]; next_length = (buffer[1] << 8) | buffer[2]; next_length += 3; @@ -97,9 +107,10 @@ unsigned long sys_io_seproxyhal_spi_send(const uint8_t *buffer, uint16_t length) ret = writeall(SEPH_FILENO, buffer, length); next_length -= length; - if (next_length == 0 && - (last_tag & SEPROXYHAL_TAG_STATUS_MASK) == SEPROXYHAL_TAG_STATUS_MASK) { - status_sent = true; + if (next_length == 0 && ((last_tag & SEPROXYHAL_TAG_STATUS_MASK) == + SEPROXYHAL_TAG_GENERAL_STATUS)) { + tx_status = true; + rx_length = 0; } return ret; @@ -115,6 +126,10 @@ unsigned long sys_io_seproxyhal_spi_recv(uint8_t *buffer, uint16_t maxlength, // fprintf(stderr, "[*] sys_io_seproxyhal_spi_recv(%p, %d, %d);\n", buffer, // maxlength, flags); + if (rx_length != 0) { + return rx_length; + } + if (maxlength < 3) { errx(1, "invalid size given to sys_io_seproxyhal_spi_recv"); } @@ -132,11 +147,12 @@ unsigned long sys_io_seproxyhal_spi_recv(uint8_t *buffer, uint16_t maxlength, _exit(1); } - status_sent = false; + tx_status = false; + rx_length = 3 + packet_size; catch_touch_info_from_seph(buffer, packet_size); - return 3 + packet_size; + return rx_length; } unsigned long sys_io_seph_recv(uint8_t *buffer, uint16_t maxlength, diff --git a/src/emulate.c b/src/emulate.c index 496482ff..d5d64638 100644 --- a/src/emulate.c +++ b/src/emulate.c @@ -38,18 +38,12 @@ int emulate(unsigned long syscall, unsigned long *parameters, case SDK_BLUE_2_2_5: retid = emulate_blue_2_2_5(syscall, parameters, ret, verbose); break; - case SDK_API_LEVEL_1: - case SDK_API_LEVEL_3: - case SDK_API_LEVEL_5: - case SDK_API_LEVEL_7: - case SDK_API_LEVEL_8: - case SDK_API_LEVEL_9: - case SDK_API_LEVEL_10: - case SDK_API_LEVEL_11: - retid = - emulate_unified_sdk(syscall, parameters, ret, verbose, version, model); - break; default: + if ((version >= SDK_API_LEVEL_1) && (version < SDK_COUNT)) { + retid = emulate_unified_sdk(syscall, parameters, ret, verbose, version, + model); + break; + } errx(1, "Unsupported SDK version %u", version); break; } diff --git a/src/emulate_lnsp_1.0.c b/src/emulate_lnsp_1.0.c index 744862c0..daead103 100644 --- a/src/emulate_lnsp_1.0.c +++ b/src/emulate_lnsp_1.0.c @@ -266,6 +266,9 @@ int emulate_nanosp_1_0(unsigned long syscall, unsigned long *parameters, SYSCALL1(cx_bn_next_prime, "(%u)", uint32_t, a); + SYSCALL5(cx_bn_gf2_n_mul, "(%u, %u, %u, %u, %u)", uint32_t, r, uint32_t, a, + uint32_t, b, uint32_t, n, uint32_t, h); + // SYSCALLs that may exists on other SDK versions, but with a different ID: SYSCALL0i(os_perso_isonboarded, os_perso_isonboarded_2_0); diff --git a/src/emulate_unified_sdk.c b/src/emulate_unified_sdk.c index f2684dff..b63e00e0 100644 --- a/src/emulate_unified_sdk.c +++ b/src/emulate_unified_sdk.c @@ -16,6 +16,10 @@ #define SYSCALL_HANDLED 1 #define SYSCALL_NOT_HANDLED 0 +// Indicates whether the XOR in the CBC mode is implemented in the CX lib +// or in the AES low level function +extern bool hdw_cbc; + /* Handle bagl related syscalls which behavior are defined in src/bolos/bagl.c */ int emulate_syscall_bagl(unsigned long syscall, unsigned long *parameters, @@ -114,12 +118,19 @@ int emulate_syscall_nbgl(unsigned long syscall, unsigned long *parameters, unsigned int, buffer_len, color_t, fore_color); - SYSCALL4(nbgl_front_draw_img_rle, "%p, %p, %u, %u", + SYSCALL4(nbgl_front_draw_img_rle_10, "%p, %p, %u, %u", nbgl_area_t *, area, uint8_t *, buffer, unsigned int, buffer_len, color_t, fore_color); + SYSCALL5(nbgl_front_draw_img_rle, "%p, %p, %u, %u, %u", + nbgl_area_t *, area, + uint8_t *, buffer, + unsigned int, buffer_len, + color_t, fore_color, + uint8_t, nb_skipped_bytes); + /* clang-format on */ default: return SYSCALL_NOT_HANDLED; @@ -159,9 +170,16 @@ int emulate_syscall_cx(unsigned long syscall, unsigned long *parameters, unsigned long *ret, bool verbose, sdk_version_t version, hw_model_t model) { - (void)version; (void)model; + // Starting from API level 12, the XOR operation of the CBC mode + // is not in CX LIB anymore + // CBC mode must be implemented + // in the AES low level functions + if (version >= SDK_API_LEVEL_12) { + hdw_cbc = true; + } + switch (syscall) { /* clang-format off */ SYSCALL0(get_api_level); @@ -517,6 +535,13 @@ int emulate_syscall_cx(unsigned long syscall, unsigned long *parameters, SYSCALL1(cx_bn_next_prime, "(%u)", uint32_t, a); + SYSCALL5(cx_bn_gf2_n_mul, "(%u, %u, %u, %u, %u)", + uint32_t, r, + uint32_t, a, + uint32_t, b, + uint32_t, n, + uint32_t, h); + SYSCALL10(cx_bls12381_key_gen, "(%u, %p, %u, %p, %u, %p, %u, %p, %p, %u)", uint8_t, mode, uint8_t *, secret, diff --git a/src/environment.c b/src/environment.c new file mode 100644 index 00000000..f9f4558a --- /dev/null +++ b/src/environment.c @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "bolos/cx.h" +#include "bolos/endorsement.h" +#include "bolos/exception.h" +#include "emulate.h" +#include "environment.h" + +/* SEED VARIABLES */ + +/* glory promote mansion idle axis finger extra february uncover one trip + * resource lawn turtle enact monster seven myth punch hobby comfort wild raise + * skin */ +static const uint8_t default_seed[MAX_SEED_SIZE] = + "\xb1\x19\x97\xfa\xff\x42\x0a\x33\x1b\xb4\xa4\xff\xdc\x8b\xdc\x8b\xa7\xc0" + "\x17\x32\xa9\x9a\x30\xd8\x3d\xbb\xeb\xd4\x69\x66\x6c\x84\xb4\x7d\x09\xd3" + "\xf5\xf4\x72\xb3\xb9\x38\x4a\xc6\x34\xbe\xba\x2a\x44\x0b\xa3\x6e\xc7\x66" + "\x11\x44\x13\x2f\x35\xe2\x06\x87\x35\x64"; + +static const char SEED_ENV_NAME[] = "SPECULOS_SEED"; + +static struct { + size_t size; + uint8_t seed[MAX_SEED_SIZE]; +} actual_seed = { 0 }; + +/* APP NAME and VERSION */ +static const char APP_NAME_VERSION_ENV_NAME[] = "SPECULOS_APPNAME"; +static const char APP_NAME_VERSION_ENV_NAME_BKP[] = "SPECULOS_DETECTED_APPNAME"; + +static env_sized_name_t app_name = { 3, "app\0" }; +static env_sized_name_t app_version = { 6, "1.33.7\0" }; + +/* RNG VARIABLES */ + +static const char RNG_ENV_NAME[] = "RNG_SEED"; +static unsigned int actual_rng = 0; + +/* ENDORSEMENT VARIABLES */ + +static const char USER_KEY_ENV_NAME[] = "USER_PRIVATE_KEY"; +static const char ATTESTATION_ENV_NAME[] = "ATTESTATION_PRIVATE_KEY"; + +static const uint8_t default_attestation_key[32] = { + 0x13, 0x8f, 0xb9, 0xb9, 0x1d, 0xa7, 0x45, 0xf1, 0x29, 0x77, 0xa2, + 0xb4, 0x6f, 0x0b, 0xce, 0x2f, 0x04, 0x18, 0xb5, 0x0f, 0xcb, 0x76, + 0x63, 0x1b, 0xaf, 0x0f, 0x08, 0xce, 0xef, 0xdb, 0x5d, 0x57 +}; + +static const uint8_t default_user_private_key[32] = { + 0xe1, 0x5e, 0x01, 0xd4, 0x70, 0x82, 0xf0, 0xea, 0x47, 0x71, 0xc9, + 0x9f, 0xe3, 0x12, 0xf9, 0xd7, 0x00, 0x93, 0xc8, 0x9a, 0xf4, 0x77, + 0x87, 0xfd, 0xf8, 0x2e, 0x03, 0x1f, 0x67, 0x28, 0xb7, 0x10 +}; + +static cx_ecfp_private_key_t attestation_key = { CX_CURVE_256K1, 32, {} }; +static cx_ecfp_private_key_t user_private_key_1 = { CX_CURVE_256K1, 32, {} }; +static cx_ecfp_private_key_t user_private_key_2 = { CX_CURVE_256K1, 32, {} }; +static env_user_certificate_t user_certificate_1 = { 0 }; +static env_user_certificate_t user_certificate_2 = { 0 }; + +/* UTILS */ + +static int unhex(uint8_t *dst, size_t dst_size, const char *src, + size_t src_size) +{ + unsigned int i; + uint8_t acc; + int8_t c; + + acc = 0; + for (i = 0; i < src_size && (i >> 1) < dst_size; i++) { + c = src[i]; + switch (c) { + case '0' ... '9': + acc = (acc << 4) + c - '0'; + break; + case 'a' ... 'f': + acc = (acc << 4) + c - 'a' + 10; + break; + case 'A' ... 'F': + acc = (acc << 4) + c - 'A' + 10; + break; + default: + return -1; + } + + if (i % 2 != 0) { + dst[i >> 1] = acc; + acc = 0; + } + } + + if (i != src_size) { + return -1; + } + + return src_size / 2; +} + +static void env_init_seed() +{ + ssize_t size; + char *p; + + p = getenv(SEED_ENV_NAME); + if (p != NULL) { + size = unhex(actual_seed.seed, sizeof(actual_seed.seed), p, strlen(p)); + if (size < 0) { + warnx("invalid seed passed through %s environment variable", + SEED_ENV_NAME); + p = NULL; + } else { + fprintf(stderr, "[*] Seed initialized from environment\n"); + } + } + + if (p == NULL) { + warnx("using default seed"); + memcpy(actual_seed.seed, default_seed, sizeof(default_seed)); + size = sizeof(default_seed); + } + actual_seed.size = size; +} + +size_t env_get_seed(uint8_t *seed, size_t max_size) +{ + memcpy(seed, actual_seed.seed, max_size); + return (actual_seed.size < max_size) ? actual_seed.size : max_size; +} + +static void env_init_rng() +{ + char *p; + p = getenv(RNG_ENV_NAME); + if (p != NULL) { + actual_rng = atoi(p); + fprintf(stderr, "[*] RNG initialized from environment: '%u'\n", actual_rng); + } else { + actual_rng = time(NULL); + } +} + +unsigned int env_get_rng() +{ + return actual_rng; +} + +static void env_init_user_hex_private_key(const char *ENV_NAME, + cx_ecfp_private_key_t *dst, + const uint8_t *default_key) +{ + ssize_t size; + char *p; + uint8_t tmp[dst->d_len]; + + p = getenv(ENV_NAME); + if (p != NULL) { + size = unhex(tmp, dst->d_len, p, strlen(p)); + if (size < 0) { + warnx("invalid user key passed through %s environment variable: not an " + "hex string", + ENV_NAME); + p = NULL; + } else if ((unsigned int)size != dst->d_len) { + warnx("invalid size for user key passed through %s environment variable: " + "expecting '%u', got '%i'", + ENV_NAME, dst->d_len, size); + p = NULL; + } + } + + if (p == NULL) { + memcpy(dst->d, default_key, dst->d_len); + } else { + memcpy(dst->d, tmp, dst->d_len); + fprintf(stderr, "[*] Private key ('%s') initialized from environment\n", + ENV_NAME); + } +} + +static void env_init_user_certificate(unsigned int index) +{ + env_user_certificate_t *certificate; + uint8_t pkey[65] = { 0 }; + sys_os_endorsement_get_public_key(index, &pkey[0]); + uint8_t hash[32] = { 0 }; + cx_sha256_t sha256; + + switch (index) { + case 1: + certificate = &user_certificate_1; + break; + case 2: + certificate = &user_certificate_2; + break; + default: + THROW(EXCEPTION); + break; + } + + sys_cx_sha256_init(&sha256); + sys_cx_hash((cx_hash_t *)&sha256, 0, (unsigned char *)"\xfe", 1, NULL, 0); + sys_cx_hash((cx_hash_t *)&sha256, CX_LAST, pkey, sizeof(pkey), hash, + sizeof(hash)); + + sys_cx_ecdsa_sign(&attestation_key, CX_RND_TRNG, CX_SHA256, hash, + sizeof(hash), certificate->buffer, MAX_CERT_SIZE, NULL); + certificate->length = certificate->buffer[1] + 2; +} + +static void env_init_endorsement() +{ + env_init_user_hex_private_key(ATTESTATION_ENV_NAME, &attestation_key, + default_attestation_key); + env_init_user_hex_private_key(USER_KEY_ENV_NAME, &user_private_key_1, + default_user_private_key); + env_init_user_certificate(1); + env_init_user_hex_private_key(USER_KEY_ENV_NAME, &user_private_key_2, + default_user_private_key); + env_init_user_certificate(2); +} + +cx_ecfp_private_key_t *env_get_user_private_key(unsigned int index) +{ + switch (index) { + case 1: + return &user_private_key_1; + break; + case 2: + return &user_private_key_2; + break; + default: + THROW(EXCEPTION); + break; + } +} + +env_user_certificate_t *env_get_user_certificate(unsigned int index) +{ + switch (index) { + case 1: + return &user_certificate_1; + break; + case 2: + return &user_certificate_2; + break; + default: + THROW(EXCEPTION); + break; + } +} + +static void env_init_app_name_version() +{ + char *str; + str = getenv(APP_NAME_VERSION_ENV_NAME); + if (str == NULL) { + str = getenv(APP_NAME_VERSION_ENV_NAME_BKP); + } + + if (str == NULL) { + warnx("using default app name & version"); + return; + } + + char *char_ptr = strchr(str, ':'); + if (char_ptr == NULL) { + warnx("Invalid ':' format in env variable '%s', falling " + "back to default.", + str); + fprintf(stderr, "[*] Default app name: '%s'\n", app_name.name); + fprintf(stderr, "[*] Default app version: '%s'\n", app_version.name); + return; + } + + app_name.length = (char_ptr - str); + app_version.length = (strlen(str) - (size_t)(app_name.length + 1)); + str[app_name.length] = '\0'; + // + 1 to include trailing '\0' + strncpy(app_name.name, str, app_name.length + 1); + strncpy(app_version.name, str + app_name.length + 1, app_version.length + 1); + + fprintf(stderr, "[*] Env app name: '%s'\n", app_name.name); + fprintf(stderr, "[*] Env app version: '%s'\n", app_version.name); +} + +size_t env_get_app_tag(char *dst, size_t length, BOLOS_TAG tag) +{ + env_sized_name_t *field; + switch (tag) { + case BOLOS_TAG_APPNAME: + field = &app_name; + break; + case BOLOS_TAG_APPVERSION: + field = &app_version; + break; + default: + return 0; + } + if (length < field->length) { + warnx("Providing length to copy env variable too small: asked for %u, " + "needs %u", + length, field->length); + return 0; + } + + strncpy(dst, field->name, length); + dst[field->length] = '\0'; + return field->length; +} + +void init_environment() +{ + env_init_seed(); + env_init_rng(); + env_init_endorsement(); + env_init_app_name_version(); +} diff --git a/src/environment.h b/src/environment.h new file mode 100644 index 00000000..d14665b3 --- /dev/null +++ b/src/environment.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#define MAX_SEED_SIZE 64 +#define MAX_STRING_SIZE 128 +#define MAX_CERT_SIZE 6 + 33 * 2 + +typedef enum { + BOLOS_TAG_APPNAME = 0x01, + BOLOS_TAG_APPVERSION = 0x02 +} BOLOS_TAG; + +typedef struct { + size_t length; + char name[MAX_STRING_SIZE]; +} env_sized_name_t; + +typedef struct { + uint8_t length; + uint8_t buffer[MAX_CERT_SIZE]; +} env_user_certificate_t; + +size_t env_get_seed(uint8_t *seed, size_t max_size); +unsigned int env_get_rng(); +cx_ecfp_private_key_t *env_get_user_private_key(unsigned int index); +env_user_certificate_t *env_get_user_certificate(unsigned int index); +size_t env_get_app_tag(char *dst, size_t length, BOLOS_TAG tag); + +void init_environment(); diff --git a/src/fonts.h b/src/fonts.h new file mode 100644 index 00000000..8ef3eeec --- /dev/null +++ b/src/fonts.h @@ -0,0 +1,170 @@ +#pragma once + +#include + +// ============================================================================ + +#define STAX_FONTS_ARRAY_ADDR 0x00805000 +// Latest (starting from SDK_API_LEVEL_15) +#define STAX_NB_FONTS 6 +// SDK_API_LEVEL_12 to SDK_API_LEVEL_14 +#define STAX_NB_FONTS_12 7 + +#define MAX_NB_FONTS STAX_NB_FONTS +#define MAX_NB_FONTS_12 STAX_NB_FONTS_12 + +// ============================================================================ +// BAGL font related structures +// (They are defined in the SDK, in lib_bagl/include/bagl.h +// ============================================================================ + +// Latest (starting from SDK_API_LEVEL_12) +typedef struct { + uint32_t encoding : 2; + uint32_t bitmap_offset : 12; + uint32_t width : 5; + uint32_t x_min_offset : 4; + uint32_t y_min_offset : 5; + uint32_t x_max_offset : 4; +} bagl_font_character_t; + +typedef struct { + uint16_t bitmap_len; + uint8_t font_id; + uint8_t bpp; + uint8_t height; + uint8_t baseline; + uint8_t first_char; + uint8_t last_char; + bagl_font_character_t *characters; + uint8_t *bitmap; +} bagl_font_t; + +// SDK_API_LEVEL_5 +typedef struct { + uint32_t char_width : 4; + uint32_t y_offset : 4; + uint32_t x_offset : 3; + uint32_t bitmap_byte_count : 5; + uint32_t bitmap_offset : 16; +} bagl_font_character_t_5; + +typedef struct { + unsigned int font_id; + unsigned char bpp; + unsigned char char_height; + unsigned char baseline_height; + signed char char_leftmost_x; + unsigned short first_char; + unsigned short last_char; + bagl_font_character_t_5 *characters; + unsigned char *bitmap; +} bagl_font_t_5; + +// SDK_API_LEVEL_1 +typedef struct { + unsigned char char_width; + unsigned char bitmap_byte_count; + unsigned short bitmap_offset; +} bagl_font_character_t_1; + +typedef struct { + unsigned int font_id; + unsigned char bpp; + unsigned char char_height; + unsigned char baseline_height; + unsigned char char_kerning; + unsigned short first_char; + unsigned short last_char; + bagl_font_character_t_1 *const characters; + unsigned char *bitmap; +} bagl_font_t_1; + +// ============================================================================ +// NBGL font related structures +// (They are defined in the SDK, in lib_nbgl/include/nbgl_fonts.h +// ============================================================================ + +// Current API_LEVEL (15) +typedef struct { + uint32_t bitmap_offset; + uint32_t encoding : 1; + uint32_t width : 6; + uint32_t x_min_offset : 4; + uint32_t y_min_offset : 6; + uint32_t x_max_offset : 4; + uint32_t y_max_offset : 6; +} nbgl_font_character_t; + +typedef struct { + uint32_t bitmap_len; + uint8_t font_id; + uint8_t bpp; + uint8_t height; + uint8_t line_height; + uint8_t char_kerning; + uint8_t crop; + uint8_t y_min; + uint8_t first_char; + uint8_t last_char; + nbgl_font_character_t *characters; + uint8_t *bitmap; +} nbgl_font_t; + +// SDK_API_LEVEL_14 +typedef struct { + uint32_t encoding : 1; + uint32_t bitmap_offset : 14; + uint32_t width : 6; + uint32_t x_min_offset : 3; + uint32_t y_min_offset : 3; + uint32_t x_max_offset : 2; + uint32_t y_max_offset : 3; +} nbgl_font_character_t_14; + +typedef struct { + uint32_t bitmap_len; + uint8_t font_id; + uint8_t bpp; + uint8_t height; + uint8_t line_height; + uint8_t char_kerning; + uint8_t crop; + uint8_t y_min; + uint8_t first_char; + uint8_t last_char; + nbgl_font_character_t_14 *characters; + uint8_t *bitmap; +} nbgl_font_t_14; + +// SDK_API_LEVEL_12 and SDK_API_LEVEL_13 +typedef struct { + uint32_t encoding : 1; + uint32_t bitmap_offset : 14; + uint32_t width : 6; + uint32_t x_min_offset : 3; + uint32_t y_min_offset : 3; + uint32_t x_max_offset : 2; + uint32_t y_max_offset : 3; +} nbgl_font_character_t_12; + +typedef struct { + uint32_t bitmap_len; + uint8_t font_id; + uint8_t bpp; + uint8_t height; + uint8_t line_height; + uint8_t crop; + uint8_t y_min; + uint8_t first_char; + uint8_t last_char; + nbgl_font_character_t_12 *characters; + uint8_t *bitmap; +} nbgl_font_t_12; + +// ============================================================================ + +void parse_fonts(void *code, unsigned long text_load_addr, + unsigned long fonts_addr, unsigned long fonts_size); + +uint32_t get_character_from_bitmap(const uint8_t *bitmap); diff --git a/src/launcher.c b/src/launcher.c index 01fb27fd..7d52b3b6 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -13,9 +13,13 @@ #include #include "emulate.h" +#include "environment.h" +#include "fonts.h" #include "svc.h" #define LOAD_ADDR ((void *)0x40000000) +#define LINK_RAM_ADDR (0xda7a0000) +#define LOAD_RAM_ADDR (0x50000000) #define MAX_APP 16 #define MAIN_APP_NAME "main" @@ -32,6 +36,8 @@ struct elf_info_s { unsigned long svc_call_addr; unsigned long svc_cx_call_addr; unsigned long text_load_addr; + unsigned long fonts_addr; + unsigned long fonts_size; }; struct app_s { @@ -126,6 +132,8 @@ static int open_app(char *name, char *filename, struct elf_info_s *elf) apps[napp].elf.svc_call_addr = elf->svc_call_addr; apps[napp].elf.svc_cx_call_addr = elf->svc_cx_call_addr; apps[napp].elf.text_load_addr = elf->text_load_addr; + apps[napp].elf.fonts_addr = elf->fonts_addr; + apps[napp].elf.fonts_size = elf->fonts_size; napp++; @@ -294,6 +302,11 @@ static void *load_app(char *name) data_addr = get_lower_page_aligned_addr(app->elf.stack_addr); data_size = get_upper_page_aligned_size( app->elf.stack_size + app->elf.stack_addr - (unsigned long)data_addr); + if (app->elf.stack_addr == LINK_RAM_ADDR) { + // Emulate RAM relocation + data_addr = (void *)LOAD_RAM_ADDR; + data_size = get_upper_page_aligned_size(app->elf.stack_size); + } /* load code * map an extra page in case the _install_params are mapped in the beginning @@ -419,31 +432,17 @@ static int load_fonts(char *fonts_path) warnx("failed to open \"%s\"", fonts_path); return -1; } - fprintf(stderr, "[*] loading fonts from \"%s\"\n", fonts_path); + + int load_size = lseek(fd, 0, SEEK_END); + lseek(fd, 0L, SEEK_SET); + + fprintf(stderr, "[*] loading fonts from \"%s\" (%d)\n", fonts_path, + load_size); int flags = MAP_PRIVATE | MAP_FIXED; int prot = PROT_READ; - int load_addr; - int load_size; - - if (sdk_version == SDK_API_LEVEL_1 || sdk_version == SDK_API_LEVEL_3 || - sdk_version == SDK_API_LEVEL_5) { - load_addr = 0x00805000; - load_size = 20480; - } else if (sdk_version == SDK_API_LEVEL_7) { - load_addr = 0x00805000; - load_size = 45056; - } else if ((sdk_version == SDK_API_LEVEL_8 || - sdk_version == SDK_API_LEVEL_9 || - sdk_version == SDK_API_LEVEL_10 || - sdk_version == SDK_API_LEVEL_11)) { - load_addr = 0x00805000; - load_size = 40960; - } else { - warn("Invalid sdk version for fonts"); - close(fd); - return -1; - } + int load_addr = STAX_FONTS_ARRAY_ADDR; + void *p = mmap((void *)load_addr, load_size, prot, flags, fd, 0); fprintf(stderr, "[*] loaded fonts at %p\n", p); @@ -533,10 +532,18 @@ static int run_app(char *name, unsigned long *parameters) app = get_current_app(); + // Parse fonts and build bitmap -> character table + parse_fonts(memory.code, app->elf.text_load_addr, app->elf.fonts_addr, + app->elf.fonts_size); + /* thumb mode */ f = (void *)((unsigned long)p | 1); stack_end = app->elf.stack_addr; - stack_start = app->elf.stack_addr + app->elf.stack_size; + if (app->elf.stack_addr == LINK_RAM_ADDR) { + // Emulate RAM relocation + stack_end = LOAD_RAM_ADDR; + } + stack_start = stack_end + app->elf.stack_size; asm volatile("mov r0, %2\n" "mov r9, %1\n" @@ -589,11 +596,12 @@ static char *parse_app_infos(char *arg, char **filename, struct elf_info_s *elf) err(1, "strdup"); } - ret = sscanf(arg, "%[^:]:%[^:]:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx", - libname, *filename, &elf->load_offset, &elf->load_size, - &elf->stack_addr, &elf->stack_size, &elf->svc_call_addr, - &elf->svc_cx_call_addr, &elf->text_load_addr); - if (ret != 9) { + ret = sscanf( + arg, "%[^:]:%[^:]:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx:0x%lx", + libname, *filename, &elf->load_offset, &elf->load_size, &elf->stack_addr, + &elf->stack_size, &elf->svc_call_addr, &elf->svc_cx_call_addr, + &elf->text_load_addr, &elf->fonts_addr, &elf->fonts_size); + if (ret != 11) { warnx("failed to parse app infos (\"%s\", %d)", arg, ret); free(libname); free(*filename); @@ -625,25 +633,15 @@ static int load_apps(int argc, char *argv[]) static sdk_version_t apilevelstr2sdkver(const char *api_level_arg) { - if (strcmp("1", api_level_arg) == 0) { - return SDK_API_LEVEL_1; - } else if (strcmp("3", api_level_arg) == 0) { - return SDK_API_LEVEL_3; - } else if (strcmp("5", api_level_arg) == 0) { - return SDK_API_LEVEL_5; - } else if (strcmp("7", api_level_arg) == 0) { - return SDK_API_LEVEL_7; - } else if (strcmp("8", api_level_arg) == 0) { - return SDK_API_LEVEL_8; - } else if (strcmp("9", api_level_arg) == 0) { - return SDK_API_LEVEL_9; - } else if (strcmp("10", api_level_arg) == 0) { - return SDK_API_LEVEL_10; - } else if (strcmp("11", api_level_arg) == 0) { - return SDK_API_LEVEL_11; - } else { + int api_level = atoi(api_level_arg); + int _sdk_version = api_level - 1 + SDK_API_LEVEL_1; + + if ((api_level <= 0) || (_sdk_version >= SDK_COUNT)) { + warnx("Invalid api level (\"%s\")", api_level_arg); return SDK_COUNT; } + + return _sdk_version; } static sdk_version_t sdkstr2sdkver(const char *sdk_arg) @@ -751,15 +749,16 @@ int main(int argc, char *argv[]) if (sdk_version == SDK_COUNT) { errx(1, "invalid SDK api_level: %s", api_level); } + fprintf(stderr, "[*] using API_LEVEL version %s on %s\n", api_level, + model_str); } else { sdk_version = sdkstr2sdkver(sdk); if (sdk_version == SDK_COUNT) { errx(1, "invalid SDK version: %s", sdk); } + fprintf(stderr, "[*] using SDK version %s on %s\n", sdk, model_str); } - fprintf(stderr, "[*] using SDK version %u on %s\n", sdk_version, model_str); - switch (hw_model) { case MODEL_NANO_S: if (sdk_version != SDK_NANO_S_1_5 && sdk_version != SDK_NANO_S_1_6 && @@ -770,7 +769,7 @@ int main(int argc, char *argv[]) case MODEL_NANO_X: if (sdk_version != SDK_NANO_X_1_2 && sdk_version != SDK_NANO_X_2_0 && sdk_version != SDK_NANO_X_2_0_2 && sdk_version != SDK_API_LEVEL_1 && - sdk_version != SDK_API_LEVEL_5) { + sdk_version != SDK_API_LEVEL_5 && sdk_version != SDK_API_LEVEL_12) { errx(1, "invalid SDK version for the Ledger Nano X"); } break; @@ -781,7 +780,8 @@ int main(int argc, char *argv[]) break; case MODEL_NANO_SP: if (sdk_version != SDK_NANO_SP_1_0 && sdk_version != SDK_NANO_SP_1_0_3 && - sdk_version != SDK_API_LEVEL_1 && sdk_version != SDK_API_LEVEL_5) { + sdk_version != SDK_API_LEVEL_1 && sdk_version != SDK_API_LEVEL_5 && + sdk_version != SDK_API_LEVEL_12) { errx(1, "invalid SDK version for the Ledger NanoSP"); } break; @@ -789,7 +789,9 @@ int main(int argc, char *argv[]) if (sdk_version != SDK_API_LEVEL_1 && sdk_version != SDK_API_LEVEL_3 && sdk_version != SDK_API_LEVEL_5 && sdk_version != SDK_API_LEVEL_7 && sdk_version != SDK_API_LEVEL_8 && sdk_version != SDK_API_LEVEL_9 && - sdk_version != SDK_API_LEVEL_10 && sdk_version != SDK_API_LEVEL_11) { + sdk_version != SDK_API_LEVEL_10 && sdk_version != SDK_API_LEVEL_11 && + sdk_version != SDK_API_LEVEL_12 && sdk_version != SDK_API_LEVEL_13 && + sdk_version != SDK_API_LEVEL_14 && sdk_version != SDK_API_LEVEL_15) { errx(1, "invalid SDK version for the Ledger Stax"); } break; @@ -808,15 +810,14 @@ int main(int argc, char *argv[]) if (sdk_version == SDK_NANO_S_2_0 || sdk_version == SDK_NANO_S_2_1 || sdk_version == SDK_NANO_X_2_0 || sdk_version == SDK_NANO_X_2_0_2 || sdk_version == SDK_NANO_SP_1_0 || sdk_version == SDK_NANO_SP_1_0_3 || - sdk_version == SDK_API_LEVEL_1 || sdk_version == SDK_API_LEVEL_3 || - sdk_version == SDK_API_LEVEL_5 || sdk_version == SDK_API_LEVEL_7 || - sdk_version == SDK_API_LEVEL_8 || sdk_version == SDK_API_LEVEL_9 || - sdk_version == SDK_API_LEVEL_10 || sdk_version == SDK_API_LEVEL_11) { + ((sdk_version >= SDK_API_LEVEL_1) && (sdk_version < SDK_COUNT))) { if (load_cxlib(cxlib_path) != 0) { return 1; } } + init_environment(); + if (hw_model == MODEL_STAX && fonts_path) { if (load_fonts(fonts_path) != 0) { return 1; diff --git a/src/sdk.h b/src/sdk.h index 44c1de39..161f74a6 100644 --- a/src/sdk.h +++ b/src/sdk.h @@ -12,16 +12,24 @@ typedef enum { SDK_BLUE_2_2_5, SDK_NANO_SP_1_0, SDK_NANO_SP_1_0_3, - // Unified SDK versions only below, do not add unrelated versions + // Unified SDK versions only below, do not add unrelated versions. + // Make sure to add all API_LEVEL as apilevelstr2sdkver() relies on it // SDK_API_LEVEL_START SDK_API_LEVEL_1, + SDK_API_LEVEL_2, SDK_API_LEVEL_3, + SDK_API_LEVEL_4, SDK_API_LEVEL_5, + SDK_API_LEVEL_6, SDK_API_LEVEL_7, SDK_API_LEVEL_8, SDK_API_LEVEL_9, SDK_API_LEVEL_10, SDK_API_LEVEL_11, + SDK_API_LEVEL_12, + SDK_API_LEVEL_13, + SDK_API_LEVEL_14, + SDK_API_LEVEL_15, SDK_COUNT } sdk_version_t; diff --git a/tests/c/CMakeLists.txt b/tests/c/CMakeLists.txt new file mode 100644 index 00000000..e9427643 --- /dev/null +++ b/tests/c/CMakeLists.txt @@ -0,0 +1,8 @@ +add_definitions(-DST31) +link_libraries(emu -lcmocka-static) + +add_executable(test_environment test_environment.c utils.c) + +add_test(NAME test_environment COMMAND qemu-arm-static test_environment WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +add_subdirectory(syscalls) diff --git a/tests/c/syscalls/CMakeLists.txt b/tests/c/syscalls/CMakeLists.txt new file mode 100644 index 00000000..b05b4248 --- /dev/null +++ b/tests/c/syscalls/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(hello hello.c) + +add_executable(test_syscall_aes test_aes.c nist_cavp.c ../utils.c) +add_executable(test_syscall_bip32 test_bip32.c ../utils.c) +add_executable(test_syscall_blake2 test_blake2.c nist_cavp.c ../utils.c) +add_executable(test_syscall_bls test_bls.c ../utils.c) +add_executable(test_syscall_bn test_bn.c) +add_executable(test_syscall_crc16 test_crc16.c) +add_executable(test_syscall_ecdh test_ecdh.c ../utils.c) +add_executable(test_syscall_ecdsa test_ecdsa.c ../utils.c) +add_executable(test_syscall_ec test_ec.c ../utils.c) +add_executable(test_syscall_ecpoint test_ecpoint.c ../utils.c) +add_executable(test_syscall_eddsa test_eddsa.c ../utils.c) +add_executable(test_syscall_eip2333 test_eip2333.c ../utils.c) +add_executable(test_syscall_endorsement test_endorsement.c) +add_executable(test_syscall_hmac test_hmac.c ../utils.c) +add_executable(test_syscall_math test_math.c) +add_executable(test_syscall_mpi_rng test_mpi_rng.c ../utils.c) +add_executable(test_syscall_os_global_pin_is_validated test_os_global_pin_is_validated.c) +add_executable(test_syscall_rfc6979 test_rfc6979.c ../utils.c) +add_executable(test_syscall_ripemd test_ripemd.c ../utils.c) +add_executable(test_syscall_sha2 test_sha2.c nist_cavp.c ../utils.c) +add_executable(test_syscall_sha3 test_sha3.c nist_cavp.c ../utils.c) +add_executable(test_syscall_slip21 test_slip21.c) + +add_test(NAME hello COMMAND qemu-arm-static hello WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +foreach(target aes bip32 blake2 bls bn crc16 ec ecpoint ecdh ecdsa eddsa endorsement hmac + math os_global_pin_is_validated rfc6979 ripemd sha2 sha3 slip21 eip2333) + add_test(NAME test_syscall_${target} COMMAND qemu-arm-static test_syscall_${target} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +endforeach() diff --git a/tests/syscalls/cavp/blake2b_kat.data b/tests/c/syscalls/cavp/blake2b_kat.data similarity index 100% rename from tests/syscalls/cavp/blake2b_kat.data rename to tests/c/syscalls/cavp/blake2b_kat.data diff --git a/tests/syscalls/cavp/groestl_224_long_msg.data b/tests/c/syscalls/cavp/groestl_224_long_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_224_long_msg.data rename to tests/c/syscalls/cavp/groestl_224_long_msg.data diff --git a/tests/syscalls/cavp/groestl_224_short_msg.data b/tests/c/syscalls/cavp/groestl_224_short_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_224_short_msg.data rename to tests/c/syscalls/cavp/groestl_224_short_msg.data diff --git a/tests/syscalls/cavp/groestl_256_long_msg.data b/tests/c/syscalls/cavp/groestl_256_long_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_256_long_msg.data rename to tests/c/syscalls/cavp/groestl_256_long_msg.data diff --git a/tests/syscalls/cavp/groestl_256_short_msg.data b/tests/c/syscalls/cavp/groestl_256_short_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_256_short_msg.data rename to tests/c/syscalls/cavp/groestl_256_short_msg.data diff --git a/tests/syscalls/cavp/groestl_384_long_msg.data b/tests/c/syscalls/cavp/groestl_384_long_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_384_long_msg.data rename to tests/c/syscalls/cavp/groestl_384_long_msg.data diff --git a/tests/syscalls/cavp/groestl_384_short_msg.data b/tests/c/syscalls/cavp/groestl_384_short_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_384_short_msg.data rename to tests/c/syscalls/cavp/groestl_384_short_msg.data diff --git a/tests/syscalls/cavp/groestl_512_long_msg.data b/tests/c/syscalls/cavp/groestl_512_long_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_512_long_msg.data rename to tests/c/syscalls/cavp/groestl_512_long_msg.data diff --git a/tests/syscalls/cavp/groestl_512_short_msg.data b/tests/c/syscalls/cavp/groestl_512_short_msg.data similarity index 100% rename from tests/syscalls/cavp/groestl_512_short_msg.data rename to tests/c/syscalls/cavp/groestl_512_short_msg.data diff --git a/tests/syscalls/cavp/hmac.data b/tests/c/syscalls/cavp/hmac.data similarity index 100% rename from tests/syscalls/cavp/hmac.data rename to tests/c/syscalls/cavp/hmac.data diff --git a/tests/syscalls/cavp/keccak_224_long_msg.data b/tests/c/syscalls/cavp/keccak_224_long_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_224_long_msg.data rename to tests/c/syscalls/cavp/keccak_224_long_msg.data diff --git a/tests/syscalls/cavp/keccak_224_short_msg.data b/tests/c/syscalls/cavp/keccak_224_short_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_224_short_msg.data rename to tests/c/syscalls/cavp/keccak_224_short_msg.data diff --git a/tests/syscalls/cavp/keccak_256_long_msg.data b/tests/c/syscalls/cavp/keccak_256_long_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_256_long_msg.data rename to tests/c/syscalls/cavp/keccak_256_long_msg.data diff --git a/tests/syscalls/cavp/keccak_256_short_msg.data b/tests/c/syscalls/cavp/keccak_256_short_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_256_short_msg.data rename to tests/c/syscalls/cavp/keccak_256_short_msg.data diff --git a/tests/syscalls/cavp/keccak_384_long_msg.data b/tests/c/syscalls/cavp/keccak_384_long_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_384_long_msg.data rename to tests/c/syscalls/cavp/keccak_384_long_msg.data diff --git a/tests/syscalls/cavp/keccak_384_short_msg.data b/tests/c/syscalls/cavp/keccak_384_short_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_384_short_msg.data rename to tests/c/syscalls/cavp/keccak_384_short_msg.data diff --git a/tests/syscalls/cavp/keccak_512_long_msg.data b/tests/c/syscalls/cavp/keccak_512_long_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_512_long_msg.data rename to tests/c/syscalls/cavp/keccak_512_long_msg.data diff --git a/tests/syscalls/cavp/keccak_512_short_msg.data b/tests/c/syscalls/cavp/keccak_512_short_msg.data similarity index 100% rename from tests/syscalls/cavp/keccak_512_short_msg.data rename to tests/c/syscalls/cavp/keccak_512_short_msg.data diff --git a/tests/syscalls/cavp/sha224_long_msg.data b/tests/c/syscalls/cavp/sha224_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha224_long_msg.data rename to tests/c/syscalls/cavp/sha224_long_msg.data diff --git a/tests/syscalls/cavp/sha224_short_msg.data b/tests/c/syscalls/cavp/sha224_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha224_short_msg.data rename to tests/c/syscalls/cavp/sha224_short_msg.data diff --git a/tests/syscalls/cavp/sha256_long_msg.data b/tests/c/syscalls/cavp/sha256_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha256_long_msg.data rename to tests/c/syscalls/cavp/sha256_long_msg.data diff --git a/tests/syscalls/cavp/sha256_short_msg.data b/tests/c/syscalls/cavp/sha256_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha256_short_msg.data rename to tests/c/syscalls/cavp/sha256_short_msg.data diff --git a/tests/syscalls/cavp/sha384_long_msg.data b/tests/c/syscalls/cavp/sha384_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha384_long_msg.data rename to tests/c/syscalls/cavp/sha384_long_msg.data diff --git a/tests/syscalls/cavp/sha384_short_msg.data b/tests/c/syscalls/cavp/sha384_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha384_short_msg.data rename to tests/c/syscalls/cavp/sha384_short_msg.data diff --git a/tests/syscalls/cavp/sha3_224_long_msg.data b/tests/c/syscalls/cavp/sha3_224_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_224_long_msg.data rename to tests/c/syscalls/cavp/sha3_224_long_msg.data diff --git a/tests/syscalls/cavp/sha3_224_short_msg.data b/tests/c/syscalls/cavp/sha3_224_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_224_short_msg.data rename to tests/c/syscalls/cavp/sha3_224_short_msg.data diff --git a/tests/syscalls/cavp/sha3_256_long_msg.data b/tests/c/syscalls/cavp/sha3_256_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_256_long_msg.data rename to tests/c/syscalls/cavp/sha3_256_long_msg.data diff --git a/tests/syscalls/cavp/sha3_256_short_msg.data b/tests/c/syscalls/cavp/sha3_256_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_256_short_msg.data rename to tests/c/syscalls/cavp/sha3_256_short_msg.data diff --git a/tests/syscalls/cavp/sha3_384_long_msg.data b/tests/c/syscalls/cavp/sha3_384_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_384_long_msg.data rename to tests/c/syscalls/cavp/sha3_384_long_msg.data diff --git a/tests/syscalls/cavp/sha3_384_short_msg.data b/tests/c/syscalls/cavp/sha3_384_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_384_short_msg.data rename to tests/c/syscalls/cavp/sha3_384_short_msg.data diff --git a/tests/syscalls/cavp/sha3_512_long_msg.data b/tests/c/syscalls/cavp/sha3_512_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_512_long_msg.data rename to tests/c/syscalls/cavp/sha3_512_long_msg.data diff --git a/tests/syscalls/cavp/sha3_512_short_msg.data b/tests/c/syscalls/cavp/sha3_512_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha3_512_short_msg.data rename to tests/c/syscalls/cavp/sha3_512_short_msg.data diff --git a/tests/syscalls/cavp/sha512_long_msg.data b/tests/c/syscalls/cavp/sha512_long_msg.data similarity index 100% rename from tests/syscalls/cavp/sha512_long_msg.data rename to tests/c/syscalls/cavp/sha512_long_msg.data diff --git a/tests/syscalls/cavp/sha512_short_msg.data b/tests/c/syscalls/cavp/sha512_short_msg.data similarity index 100% rename from tests/syscalls/cavp/sha512_short_msg.data rename to tests/c/syscalls/cavp/sha512_short_msg.data diff --git a/tests/syscalls/cavp/shake128_long_msg.data b/tests/c/syscalls/cavp/shake128_long_msg.data similarity index 100% rename from tests/syscalls/cavp/shake128_long_msg.data rename to tests/c/syscalls/cavp/shake128_long_msg.data diff --git a/tests/syscalls/cavp/shake128_short_msg.data b/tests/c/syscalls/cavp/shake128_short_msg.data similarity index 100% rename from tests/syscalls/cavp/shake128_short_msg.data rename to tests/c/syscalls/cavp/shake128_short_msg.data diff --git a/tests/syscalls/cavp/shake128_variable_output.data b/tests/c/syscalls/cavp/shake128_variable_output.data similarity index 100% rename from tests/syscalls/cavp/shake128_variable_output.data rename to tests/c/syscalls/cavp/shake128_variable_output.data diff --git a/tests/syscalls/cavp/shake256_long_msg.data b/tests/c/syscalls/cavp/shake256_long_msg.data similarity index 100% rename from tests/syscalls/cavp/shake256_long_msg.data rename to tests/c/syscalls/cavp/shake256_long_msg.data diff --git a/tests/syscalls/cavp/shake256_short_msg.data b/tests/c/syscalls/cavp/shake256_short_msg.data similarity index 100% rename from tests/syscalls/cavp/shake256_short_msg.data rename to tests/c/syscalls/cavp/shake256_short_msg.data diff --git a/tests/syscalls/cavp/shake256_variable_output.data b/tests/c/syscalls/cavp/shake256_variable_output.data similarity index 100% rename from tests/syscalls/cavp/shake256_variable_output.data rename to tests/c/syscalls/cavp/shake256_variable_output.data diff --git a/tests/syscalls/hello.c b/tests/c/syscalls/hello.c similarity index 100% rename from tests/syscalls/hello.c rename to tests/c/syscalls/hello.c diff --git a/tests/syscalls/nist_cavp.c b/tests/c/syscalls/nist_cavp.c similarity index 99% rename from tests/syscalls/nist_cavp.c rename to tests/c/syscalls/nist_cavp.c index fdfdbb2c..8a467b01 100644 --- a/tests/syscalls/nist_cavp.c +++ b/tests/c/syscalls/nist_cavp.c @@ -6,9 +6,9 @@ // must come after setjmp.h #include +#include "../utils.h" #include "bolos/cx.h" #include "nist_cavp.h" -#include "utils.h" #define CX_MAX_DIGEST_SIZE CX_SHA512_SIZE diff --git a/tests/syscalls/nist_cavp.h b/tests/c/syscalls/nist_cavp.h similarity index 100% rename from tests/syscalls/nist_cavp.h rename to tests/c/syscalls/nist_cavp.h diff --git a/tests/syscalls/test_aes.c b/tests/c/syscalls/test_aes.c similarity index 99% rename from tests/syscalls/test_aes.c rename to tests/c/syscalls/test_aes.c index a5ffe442..7fe3ca04 100644 --- a/tests/syscalls/test_aes.c +++ b/tests/c/syscalls/test_aes.c @@ -37,7 +37,7 @@ print(binascii.hexlify(dt)) #include "bolos/cx.h" #include "emulate.h" -#include "utils.h" +#include "../utils.h" void test_aes_cbc1(void **state __attribute__((unused))) { diff --git a/tests/syscalls/test_bip32.c b/tests/c/syscalls/test_bip32.c similarity index 99% rename from tests/syscalls/test_bip32.c rename to tests/c/syscalls/test_bip32.c index 95791712..b76a6bc2 100644 --- a/tests/syscalls/test_bip32.c +++ b/tests/c/syscalls/test_bip32.c @@ -7,11 +7,13 @@ #include +#include "../utils.h" + #include "bolos/cx.h" #include "bolos/cx_utils.h" #include "bolos/os_bip32.h" #include "emulate.h" -#include "utils.h" +#include "environment.h" #define MAX_CHAIN_LEN 5 @@ -578,6 +580,7 @@ static void test_bip32_vector(const bip32_test_vector *v) memset(&extkey, 0, sizeof(extkey)); assert_int_equal(setenv("SPECULOS_SEED", v->seed, 1), 0); + init_environment(); for (i = 0; i < v->chain_len; i++) { path[i] = v->chain[i].index; @@ -599,7 +602,7 @@ static void test_bip32_vector(const bip32_test_vector *v) static void test_bip32(void **state __attribute__((unused))) { size_t i; - + init_environment(); for (i = 0; i < ARRAY_SIZE(test_vectors); i++) { test_bip32_vector(&test_vectors[i]); } @@ -614,6 +617,7 @@ static void test_bolos_vector(const struct bolos_vector *v) size_t sk_length; ssize_t path_len; uint8_t *p; + init_environment(); switch (v->mode) { case 0: diff --git a/tests/syscalls/test_blake2.c b/tests/c/syscalls/test_blake2.c similarity index 95% rename from tests/syscalls/test_blake2.c rename to tests/c/syscalls/test_blake2.c index ee28f3d3..d9a0c40e 100644 --- a/tests/syscalls/test_blake2.c +++ b/tests/c/syscalls/test_blake2.c @@ -4,9 +4,9 @@ // must come after setjmp.h #include +#include "../utils.h" #include "bolos/cx.h" #include "nist_cavp.h" -#include "utils.h" void test_blake2b_kat(void **state __attribute__((unused))) { diff --git a/tests/syscalls/test_bls.c b/tests/c/syscalls/test_bls.c similarity index 99% rename from tests/syscalls/test_bls.c rename to tests/c/syscalls/test_bls.c index 9a78f919..b73bbdea 100644 --- a/tests/syscalls/test_bls.c +++ b/tests/c/syscalls/test_bls.c @@ -9,10 +9,10 @@ #include #include +#include "../utils.h" #include "bolos/cx.h" #include "bolos/cx_bls.h" #include "bolos/cxlib.h" -#include "utils.h" #define BLS_DERIVE_SECRET (0x80) #define BLS_DST_AUG ("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_") diff --git a/tests/c/syscalls/test_bn.c b/tests/c/syscalls/test_bn.c new file mode 100644 index 00000000..4d8631b5 --- /dev/null +++ b/tests/c/syscalls/test_bn.c @@ -0,0 +1,75 @@ +#include +#include +#include +// must come after setjmp.h +#include + +#include "bolos/cxlib.h" + +#define GF2_8_MPI_BYTES 1 + +static void test_cx_bn_gf2_n_mul(void **state __attribute__((unused))) +{ + cx_err_t error = CX_OK; + + cx_bn_t a, // multiplicand + b, // multiplier + m, // modulus + r, // result + r2; // Montgomery constant + + // A(x) + const uint8_t multiplicand[1] = { 0x1B }; // 27 + // B(x) + const uint8_t multiplier[1] = { 0x3D }; // 61 + + // The irreducible polynomial N(x) = x^8 + x^4 + x^3 + x + 1 + const uint8_t N[2] = { 0x01, 0x1B }; // 283 + + // 2nd Montgomery constant: R2 = x^(2*t*8) mod N(x) + // t = 1 since the number of bytes of R is 1. + const uint8_t R2[1] = { 0x56 }; + + // Expected result of N(x) = A(x )* B(x) + const uint32_t re = 0x49; // result expected = 71 + int diff; + + CX_CHECK(sys_cx_bn_lock(GF2_8_MPI_BYTES, 0)); + CX_CHECK(sys_cx_bn_alloc(&r, GF2_8_MPI_BYTES)); + CX_CHECK(sys_cx_bn_alloc_init(&a, GF2_8_MPI_BYTES, multiplicand, + sizeof(multiplicand))); + CX_CHECK(sys_cx_bn_alloc_init(&b, GF2_8_MPI_BYTES, multiplier, + sizeof(multiplier))); + CX_CHECK(sys_cx_bn_alloc_init(&m, GF2_8_MPI_BYTES, N, sizeof(N))); + CX_CHECK(sys_cx_bn_alloc_init(&r2, GF2_8_MPI_BYTES, R2, sizeof(R2))); + + // Perform the Galois Field GF(2m) multiplication operation + CX_CHECK(sys_cx_bn_gf2_n_mul(r, a, b, m, r2)); + + // Compare result to expected result + CX_CHECK(sys_cx_bn_cmp_u32(r, re, &diff)); + + CX_CHECK(sys_cx_bn_destroy(&r)); + CX_CHECK(sys_cx_bn_destroy(&a)); + CX_CHECK(sys_cx_bn_destroy(&b)); + CX_CHECK(sys_cx_bn_destroy(&m)); + CX_CHECK(sys_cx_bn_destroy(&r2)); + +end: + if (sys_cx_bn_is_locked()) { + sys_cx_bn_unlock(); + } + + // Assert that there are no errors + assert_int_equal(error, CX_OK); + + // Assert that the result is correct + assert_int_equal(diff, 0); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { cmocka_unit_test(test_cx_bn_gf2_n_mul) }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/syscalls/test_crc16.c b/tests/c/syscalls/test_crc16.c similarity index 100% rename from tests/syscalls/test_crc16.c rename to tests/c/syscalls/test_crc16.c diff --git a/tests/syscalls/test_ec.c b/tests/c/syscalls/test_ec.c similarity index 99% rename from tests/syscalls/test_ec.c rename to tests/c/syscalls/test_ec.c index 46760abb..994c759c 100644 --- a/tests/syscalls/test_ec.c +++ b/tests/c/syscalls/test_ec.c @@ -7,8 +7,8 @@ // must come after setjmp.h #include +#include "../utils.h" #include "bolos/cx.h" -#include "utils.h" static uint8_t const C_ED25519_G[] = { // uncompressed diff --git a/tests/syscalls/test_ecdh.c b/tests/c/syscalls/test_ecdh.c similarity index 99% rename from tests/syscalls/test_ecdh.c rename to tests/c/syscalls/test_ecdh.c index 210542b6..0232dc1d 100644 --- a/tests/syscalls/test_ecdh.c +++ b/tests/c/syscalls/test_ecdh.c @@ -7,8 +7,8 @@ // must come after setjmp.h #include +#include "../utils.h" #include "nist_cavp.h" -#include "utils.h" #include "bolos/cx.h" #include "emulate.h" diff --git a/tests/syscalls/test_ecdsa.c b/tests/c/syscalls/test_ecdsa.c similarity index 99% rename from tests/syscalls/test_ecdsa.c rename to tests/c/syscalls/test_ecdsa.c index f9954f16..d234022d 100644 --- a/tests/syscalls/test_ecdsa.c +++ b/tests/c/syscalls/test_ecdsa.c @@ -9,9 +9,9 @@ #include +#include "../utils.h" #include "bolos/cx.h" #include "emulate.h" -#include "utils.h" #define cx_ecfp_init_private_key sys_cx_ecfp_init_private_key #define cx_ecfp_generate_pair sys_cx_ecfp_generate_pair diff --git a/tests/syscalls/test_ecpoint.c b/tests/c/syscalls/test_ecpoint.c similarity index 99% rename from tests/syscalls/test_ecpoint.c rename to tests/c/syscalls/test_ecpoint.c index 468c3eab..43e224c4 100644 --- a/tests/syscalls/test_ecpoint.c +++ b/tests/c/syscalls/test_ecpoint.c @@ -9,9 +9,10 @@ #define _SDK_2_0_ +#include "../utils.h" + #include "bolos/cx.h" #include "bolos/cxlib.h" -#include "utils.h" #define WYCH_MAX_LINE_LENGTH (360) #define MONT_CURVE_MAX_LENGTH (56) diff --git a/tests/syscalls/test_eddsa.c b/tests/c/syscalls/test_eddsa.c similarity index 99% rename from tests/syscalls/test_eddsa.c rename to tests/c/syscalls/test_eddsa.c index 15ae4127..f6e36895 100644 --- a/tests/syscalls/test_eddsa.c +++ b/tests/c/syscalls/test_eddsa.c @@ -8,7 +8,7 @@ #include "bolos/cx.h" -#include "utils.h" +#include "../utils.h" #define cx_ecfp_init_private_key sys_cx_ecfp_init_private_key #define cx_ecfp_generate_pair sys_cx_ecfp_generate_pair diff --git a/tests/syscalls/test_eip2333.c b/tests/c/syscalls/test_eip2333.c similarity index 98% rename from tests/syscalls/test_eip2333.c rename to tests/c/syscalls/test_eip2333.c index 263aca2f..0661f416 100644 --- a/tests/syscalls/test_eip2333.c +++ b/tests/c/syscalls/test_eip2333.c @@ -8,10 +8,11 @@ #include +#include "../utils.h" #include "bolos/cx.h" #include "bolos/cx_utils.h" #include "emulate.h" -#include "utils.h" +#include "environment.h" #define MAX_PATH_LEN 10 @@ -228,6 +229,7 @@ static void test_eip_vector(const eip2333_test_vector *v) int path_len; assert_int_equal(setenv("SPECULOS_SEED", v->seed, 1), 0); + init_environment(); path_len = get_path(v->path, path, MAX_PATH_LEN); assert_int_equal(path_len, 1); @@ -245,6 +247,7 @@ static void test_eip2333_derive(void **state __attribute__((unused))) unsigned int i; assert_int_equal(setenv("SPECULOS_SEED", default_seed, 1), 0); + init_environment(); for (i = 0; i < ARRAY_SIZE(test_vectors); i++) { test_eip_vector(&test_vectors[i]); @@ -257,6 +260,7 @@ static void test_bolos_vector(const bolos_test_vector *v) unsigned int path[10]; int path_len; + init_environment(); path_len = get_path(v->path, path, MAX_PATH_LEN); assert_int_equal(path_len, v->path_len); diff --git a/tests/syscalls/test_endorsement.c b/tests/c/syscalls/test_endorsement.c similarity index 99% rename from tests/syscalls/test_endorsement.c rename to tests/c/syscalls/test_endorsement.c index 78afe00f..737822e5 100644 --- a/tests/syscalls/test_endorsement.c +++ b/tests/c/syscalls/test_endorsement.c @@ -6,10 +6,12 @@ #include +#include "../utils.h" + #include "bolos/cx.h" #include "bolos/endorsement.h" #include "emulate.h" -#include "utils.h" +#include "environment.h" #define cx_ecfp_init_public_key sys_cx_ecfp_init_public_key #define cx_hash sys_cx_hash @@ -25,6 +27,8 @@ void test_endorsement(void **state __attribute__((unused))) { + init_environment(); + uint8_t raw_endorsement_pubkey[65] = { 0 }; uint8_t endorsement_sig[80] = { 0 }; uint8_t endorsement_key1_sig[80] = { 0 }; diff --git a/tests/syscalls/test_hmac.c b/tests/c/syscalls/test_hmac.c similarity index 99% rename from tests/syscalls/test_hmac.c rename to tests/c/syscalls/test_hmac.c index cc17facb..2cc320e7 100644 --- a/tests/syscalls/test_hmac.c +++ b/tests/c/syscalls/test_hmac.c @@ -7,7 +7,7 @@ // must come after setjmp.h #include -#include "utils.h" +#include "../utils.h" #include "bolos/cx.h" #include "emulate.h" diff --git a/tests/syscalls/test_math.c b/tests/c/syscalls/test_math.c similarity index 99% rename from tests/syscalls/test_math.c rename to tests/c/syscalls/test_math.c index 6c3f865d..203911b6 100644 --- a/tests/syscalls/test_math.c +++ b/tests/c/syscalls/test_math.c @@ -5,7 +5,7 @@ // must come after setjmp.h #include -#include "utils.h" +#include "../utils.h" #include "bolos/cx.h" #include "emulate.h" diff --git a/tests/syscalls/test_mpi_rng.c b/tests/c/syscalls/test_mpi_rng.c similarity index 94% rename from tests/syscalls/test_mpi_rng.c rename to tests/c/syscalls/test_mpi_rng.c index 68c258bd..89d7bedd 100644 --- a/tests/syscalls/test_mpi_rng.c +++ b/tests/c/syscalls/test_mpi_rng.c @@ -9,8 +9,8 @@ #include #include +#include "../utils.h" #include "bolos/cxlib.h" -#include "utils.h" unsigned long sys_cx_rng(uint8_t *buffer, unsigned int length); @@ -19,8 +19,6 @@ void test_mpi_rng(void **state __attribute__((unused))) cx_mpi_t *r, *n; int ret; cx_err_t error; - uint32_t i, j, rnd, value; - uint8_t buffer[64]; // Those tests will check that cx_mpi_rng is working as expected: // cx_mpi_rng(r,n) => Generate a random value r in the range 0 < r < n. diff --git a/tests/syscalls/test_os_global_pin_is_validated.c b/tests/c/syscalls/test_os_global_pin_is_validated.c similarity index 98% rename from tests/syscalls/test_os_global_pin_is_validated.c rename to tests/c/syscalls/test_os_global_pin_is_validated.c index b220b603..5f81c289 100644 --- a/tests/syscalls/test_os_global_pin_is_validated.c +++ b/tests/c/syscalls/test_os_global_pin_is_validated.c @@ -4,9 +4,9 @@ #include +#include "../utils.h" #include "emulate.h" #include "sdk.h" -#include "utils.h" #define os_os_global_pin_is_validated sys_os_global_pin_is_validated diff --git a/tests/syscalls/test_rfc6979.c b/tests/c/syscalls/test_rfc6979.c similarity index 99% rename from tests/syscalls/test_rfc6979.c rename to tests/c/syscalls/test_rfc6979.c index 47645fd2..a83d9f4e 100644 --- a/tests/syscalls/test_rfc6979.c +++ b/tests/c/syscalls/test_rfc6979.c @@ -7,9 +7,9 @@ // must come after setjmp.h #include +#include "../utils.h" #include "bolos/cx.h" #include "emulate.h" -#include "utils.h" // Test vectors from https://tools.ietf.org/html/rfc6979#appendix-A.2 typedef struct { diff --git a/tests/syscalls/test_ripemd.c b/tests/c/syscalls/test_ripemd.c similarity index 99% rename from tests/syscalls/test_ripemd.c rename to tests/c/syscalls/test_ripemd.c index e0f35356..42d7015f 100644 --- a/tests/syscalls/test_ripemd.c +++ b/tests/c/syscalls/test_ripemd.c @@ -7,7 +7,7 @@ // must come after setjmp.h #include -#include "utils.h" +#include "../utils.h" #include "bolos/cx.h" #include "emulate.h" diff --git a/tests/syscalls/test_sha2.c b/tests/c/syscalls/test_sha2.c similarity index 99% rename from tests/syscalls/test_sha2.c rename to tests/c/syscalls/test_sha2.c index 03bfdcb3..1d179932 100644 --- a/tests/syscalls/test_sha2.c +++ b/tests/c/syscalls/test_sha2.c @@ -7,8 +7,8 @@ // must come after setjmp.h #include +#include "../utils.h" #include "nist_cavp.h" -#include "utils.h" #include "bolos/cx.h" #include "emulate.h" diff --git a/tests/syscalls/test_sha3.c b/tests/c/syscalls/test_sha3.c similarity index 99% rename from tests/syscalls/test_sha3.c rename to tests/c/syscalls/test_sha3.c index f4490e97..d525d7ba 100644 --- a/tests/syscalls/test_sha3.c +++ b/tests/c/syscalls/test_sha3.c @@ -6,9 +6,9 @@ // must come after setjmp.h #include +#include "../utils.h" #include "bolos/cx.h" #include "nist_cavp.h" -#include "utils.h" /* // Exception-related functions. Exceptions are not handled, functions are just diff --git a/tests/syscalls/test_slip21.c b/tests/c/syscalls/test_slip21.c similarity index 96% rename from tests/syscalls/test_slip21.c rename to tests/c/syscalls/test_slip21.c index 27e2a8b7..a994be05 100644 --- a/tests/syscalls/test_slip21.c +++ b/tests/c/syscalls/test_slip21.c @@ -9,6 +9,7 @@ #include "bolos/cx.h" #include "bolos/os_bip32.h" #include "emulate.h" +#include "environment.h" void test_slip21(void **state __attribute__((unused))) { @@ -30,6 +31,8 @@ void test_slip21(void **state __attribute__((unused))) 1), 0); + init_environment(); + sys_os_perso_derive_node_with_seed_key(HDW_SLIP21, CX_CURVE_SECP256K1, (uint32_t *)SLIP77_LABEL, 10, key, NULL, NULL, 0); diff --git a/tests/syscalls/wycheproof/X25519.data b/tests/c/syscalls/wycheproof/X25519.data similarity index 100% rename from tests/syscalls/wycheproof/X25519.data rename to tests/c/syscalls/wycheproof/X25519.data diff --git a/tests/syscalls/wycheproof/X448.data b/tests/c/syscalls/wycheproof/X448.data similarity index 100% rename from tests/syscalls/wycheproof/X448.data rename to tests/c/syscalls/wycheproof/X448.data diff --git a/tests/syscalls/wycheproof/ecdh_secp256k1.data b/tests/c/syscalls/wycheproof/ecdh_secp256k1.data similarity index 100% rename from tests/syscalls/wycheproof/ecdh_secp256k1.data rename to tests/c/syscalls/wycheproof/ecdh_secp256k1.data diff --git a/tests/syscalls/wycheproof/eddsa.data b/tests/c/syscalls/wycheproof/eddsa.data similarity index 100% rename from tests/syscalls/wycheproof/eddsa.data rename to tests/c/syscalls/wycheproof/eddsa.data diff --git a/tests/c/test_environment.c b/tests/c/test_environment.c new file mode 100644 index 00000000..4aa6dffa --- /dev/null +++ b/tests/c/test_environment.c @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils.h" + +#include "bolos/cx.h" +#include "bolos/endorsement.h" +#include "environment.h" + +static const char *SEED_ENV_NAME = "SPECULOS_SEED"; +static const char *APP_NAME_VERSION_ENV_NAME = "SPECULOS_APPNAME"; +static const char *APP_NAME_VERSION_ENV_NAME_BKP = "SPECULOS_DETECTED_APPNAME"; +static const char *RNG_ENV_NAME = "RNG_SEED"; +static const char *USER_KEY_ENV_NAME = "USER_PRIVATE_KEY"; +static const char *ATTESTATION_ENV_NAME = "ATTESTATION_PRIVATE_KEY"; +static uint8_t default_seed[MAX_SEED_SIZE] = + "\xb1\x19\x97\xfa\xff\x42\x0a\x33\x1b\xb4\xa4\xff\xdc\x8b\xdc\x8b\xa7\xc0" + "\x17\x32\xa9\x9a\x30\xd8\x3d\xbb\xeb\xd4\x69\x66\x6c\x84\xb4\x7d\x09\xd3" + "\xf5\xf4\x72\xb3\xb9\x38\x4a\xc6\x34\xbe\xba\x2a\x44\x0b\xa3\x6e\xc7\x66" + "\x11\x44\x13\x2f\x35\xe2\x06\x87\x35\x64"; +static char *default_app_name = "app"; +static char *default_app_version = "1.33.7"; + +static uint8_t default_user_private_key[KEY_LENGTH] = { + 0xe1, 0x5e, 0x01, 0xd4, 0x70, 0x82, 0xf0, 0xea, 0x47, 0x71, 0xc9, + 0x9f, 0xe3, 0x12, 0xf9, 0xd7, 0x00, 0x93, 0xc8, 0x9a, 0xf4, 0x77, + 0x87, 0xfd, 0xf8, 0x2e, 0x03, 0x1f, 0x67, 0x28, 0xb7, 0x10 +}; +#define MAX_CERT_LENGTH 75 +static const uint8_t default_user_certificate[MAX_CERT_LENGTH] = { + 0x30, 0x45, 0x02, 0x21, 0x00, 0xbf, 0x23, 0x7e, 0x5b, 0x40, 0x06, 0x14, + 0x17, 0xf6, 0x62, 0xa6, 0xd0, 0x8a, 0x4b, 0xde, 0x1f, 0xe3, 0x34, 0x3b, + 0xd8, 0x70, 0x8c, 0xed, 0x04, 0x6c, 0x84, 0x17, 0x49, 0x5a, 0xd3, 0x6c, + 0xcf, 0x02, 0x20, 0x3d, 0x39, 0xa5, 0x32, 0xee, 0xca, 0xdf, 0xf6, 0xdf, + 0x20, 0x53, 0xe4, 0xab, 0x98, 0x96, 0xaa, 0x00, 0xf3, 0xbe, 0xf1, 0x5c, + 0x4b, 0xd1, 0x1c, 0x53, 0x66, 0x1e, 0x54, 0xfe, 0x5e, 0x2f, 0xf4 +}; + +typedef enum { + FIELD_SEED, + FIELD_RNG, + FIELD_USER_KEY, + FIELD_APPNAME, + FIELD_APPVERSION +} field_e; + +static void check_is_default(field_e field) +{ + switch (field) { + case FIELD_SEED: { + uint8_t key[MAX_SEED_SIZE] = {}; + env_get_seed(&key[0], MAX_SEED_SIZE); + assert_memory_equal(&key[0], default_seed, MAX_SEED_SIZE); + break; + } + case FIELD_RNG: { + unsigned int now = time(NULL); + // RNG is initialized in the past, not that long ago, but still + // assert_int_equal(env_get_rng(), time(NULL)); can sometimes fail + assert_in_range(env_get_rng(), now - 1, now); + break; + } + case FIELD_USER_KEY: { + assert_memory_equal(env_get_user_private_key(1)->d, + default_user_private_key, + env_get_user_private_key(1)->d_len); + assert_memory_equal(env_get_user_private_key(2)->d, + default_user_private_key, + env_get_user_private_key(2)->d_len); + assert_memory_equal(env_get_user_certificate(1)->buffer, + default_user_certificate, + env_get_user_certificate(1)->length); + assert_memory_equal(env_get_user_certificate(2)->buffer, + default_user_certificate, + env_get_user_certificate(2)->length); + break; + } + case FIELD_APPNAME: { + char buffer[10]; + size_t size = env_get_app_tag(buffer, 10, BOLOS_TAG_APPNAME); + assert_string_equal(buffer, default_app_name); + assert_int_equal(size, strlen(buffer)); + break; + } + case FIELD_APPVERSION: { + char buffer[10]; + size_t size = env_get_app_tag(buffer, 10, BOLOS_TAG_APPVERSION); + assert_string_equal(buffer, default_app_version); + assert_int_equal(size, strlen(buffer)); + break; + } + default: + assert_true(false); + break; + } +} + +static void test_complete_default(void **state __attribute__((unused))) +{ + init_environment(); + check_is_default(FIELD_SEED); + check_is_default(FIELD_RNG); + check_is_default(FIELD_USER_KEY); + check_is_default(FIELD_APPNAME); + check_is_default(FIELD_APPVERSION); +} + +static void test_change_seed(void **state __attribute__((unused))) +{ + char *seed = "0123456789abcdef"; + uint8_t expected_seed[8]; + uint8_t result_seed[8]; + hexstr2bin(seed, expected_seed, sizeof(expected_seed)); + + setenv(SEED_ENV_NAME, seed, true); + init_environment(); + + env_get_seed(result_seed, 8); + assert_memory_equal(expected_seed, result_seed, 8); + + check_is_default(FIELD_RNG); + check_is_default(FIELD_USER_KEY); + check_is_default(FIELD_APPNAME); + check_is_default(FIELD_APPVERSION); +} + +static void test_change_rng(void **state __attribute__((unused))) +{ + char *rng_str = "7654"; + int rng_int = atoi(rng_str); + + setenv(RNG_ENV_NAME, rng_str, true); + + init_environment(); + assert_int_equal(env_get_rng(), rng_int); + + check_is_default(FIELD_SEED); + check_is_default(FIELD_USER_KEY); + check_is_default(FIELD_APPNAME); + check_is_default(FIELD_APPVERSION); +} + +static void test_change_attestation(void **state __attribute__((unused))) +{ + setenv(ATTESTATION_ENV_NAME, + "0123456789000000000000000000000000000000000000000000000000abcdef", + true); + init_environment(); + + assert_memory_equal(env_get_user_private_key(1)->d, default_user_private_key, + env_get_user_private_key(1)->d_len); + assert_memory_equal(env_get_user_private_key(2)->d, default_user_private_key, + env_get_user_private_key(2)->d_len); + assert_memory_not_equal(env_get_user_certificate(1)->buffer, + default_user_certificate, + env_get_user_certificate(1)->length); + assert_memory_not_equal(env_get_user_certificate(2)->buffer, + default_user_certificate, + env_get_user_certificate(2)->length); + + check_is_default(FIELD_SEED); + check_is_default(FIELD_RNG); + check_is_default(FIELD_APPNAME); + check_is_default(FIELD_APPVERSION); +} + +static void test_change_user_key(void **state __attribute__((unused))) +{ + setenv(USER_KEY_ENV_NAME, + "abcdef0000000000000000000000000000000000000000000000000123456789", + true); + init_environment(); + + assert_memory_not_equal(env_get_user_private_key(1)->d, + default_user_private_key, + env_get_user_private_key(1)->d_len); + assert_memory_not_equal(env_get_user_private_key(2)->d, + default_user_private_key, + env_get_user_private_key(2)->d_len); + assert_memory_not_equal(env_get_user_certificate(1)->buffer, + default_user_certificate, + env_get_user_certificate(1)->length); + assert_memory_not_equal(env_get_user_certificate(2)->buffer, + default_user_certificate, + env_get_user_certificate(2)->length); + + check_is_default(FIELD_SEED); + check_is_default(FIELD_RNG); + check_is_default(FIELD_APPNAME); + check_is_default(FIELD_APPVERSION); +} + +static void test_change_appname_appversion(void **state __attribute__((unused))) +{ + char *name = "some_app"; + char *version = "some_version"; + // + 1 for ':' then + 1 for '\0' + char value[strlen(name) + strlen(version) + 1 + 1]; + snprintf(value, sizeof(value), "%s:%s", name, version); + + setenv(APP_NAME_VERSION_ENV_NAME, value, true); + init_environment(); + + char buffer[strlen(name) < strlen(version) ? strlen(version) : strlen(name)]; + size_t str_len = 0; + str_len = env_get_app_tag(buffer, sizeof(buffer), BOLOS_TAG_APPNAME); + assert_string_equal(buffer, name); + assert_int_equal(str_len, strlen(name)); + str_len = env_get_app_tag(buffer, sizeof(buffer), BOLOS_TAG_APPVERSION); + assert_string_equal(buffer, version); + assert_int_equal(str_len, strlen(version)); + + check_is_default(FIELD_SEED); + check_is_default(FIELD_RNG); + check_is_default(FIELD_USER_KEY); +} + +static int setup(void **state __attribute__((unused))) +{ + unsetenv(SEED_ENV_NAME); + unsetenv(APP_NAME_VERSION_ENV_NAME); + unsetenv(APP_NAME_VERSION_ENV_NAME_BKP); + unsetenv(RNG_ENV_NAME); + unsetenv(USER_KEY_ENV_NAME); + unsetenv(ATTESTATION_ENV_NAME); + return 0; +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(test_complete_default, setup), + cmocka_unit_test_setup(test_change_seed, setup), + cmocka_unit_test_setup(test_change_rng, setup), + cmocka_unit_test_setup(test_change_attestation, setup), + cmocka_unit_test_setup(test_change_user_key, setup), + cmocka_unit_test_setup(test_change_appname_appversion, setup), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/syscalls/utils.c b/tests/c/utils.c similarity index 100% rename from tests/syscalls/utils.c rename to tests/c/utils.c diff --git a/tests/syscalls/utils.h b/tests/c/utils.h similarity index 80% rename from tests/syscalls/utils.h rename to tests/c/utils.h index 39ebde09..6209d0e4 100644 --- a/tests/syscalls/utils.h +++ b/tests/c/utils.h @@ -3,7 +3,7 @@ #include #include -#define TESTS_PATH "../../../tests/syscalls/" +#define TESTS_PATH "../../../../tests/c/syscalls/" #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) int hex2num(char c); diff --git a/tests/__init__.py b/tests/python/__init__.py similarity index 100% rename from tests/__init__.py rename to tests/python/__init__.py diff --git a/tests/api/resources/automation.json b/tests/python/api/resources/automation.json similarity index 100% rename from tests/api/resources/automation.json rename to tests/python/api/resources/automation.json diff --git a/tests/api/test_api.py b/tests/python/api/test_api.py similarity index 89% rename from tests/api/test_api.py rename to tests/python/api/test_api.py index a43b3667..1e3fb2a3 100644 --- a/tests/api/test_api.py +++ b/tests/python/api/test_api.py @@ -9,27 +9,9 @@ from speculos.client import SpeculosClient -AppInfo = namedtuple("AppInfo", ["filepath", "device", "name", "version", "hash"]) - -SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) API_URL = "http://127.0.0.1:5000" -@pytest.fixture(scope="class") -def client(request): - """Run the API tests on the default btc.elf app.""" - - app_dir = os.path.join(SCRIPT_DIR, os.pardir, os.pardir, "apps") - filepath = os.path.realpath(os.path.join(app_dir, "btc.elf")) - info = [filepath] + os.path.basename(filepath).split("#") - info = AppInfo(*info) - - args = ["--model", info.device, "--sdk", info.version] - - with SpeculosClient(app=filepath, args=args) as _client: - yield _client - - @pytest.mark.usefixtures("client") class TestApi: @staticmethod @@ -109,7 +91,7 @@ def get_current_screen_content(session): assert re.match(text, event["text"]) texts = [("About",), ("Version", ".*"), ("Bitcoin", "is ready")] - for i in range(0, 3): + for i in range(len(texts)): TestApi.press_button("left") for text in texts[i]: event = get_next_event(stream) diff --git a/tests/apps/__init__.py b/tests/python/apps/__init__.py similarity index 100% rename from tests/apps/__init__.py rename to tests/python/apps/__init__.py diff --git a/tests/apps/resources/__init__.py b/tests/python/apps/resources/__init__.py similarity index 100% rename from tests/apps/resources/__init__.py rename to tests/python/apps/resources/__init__.py diff --git a/tests/apps/resources/btc_getpubkey_blue.json b/tests/python/apps/resources/btc_getpubkey_blue.json similarity index 100% rename from tests/apps/resources/btc_getpubkey_blue.json rename to tests/python/apps/resources/btc_getpubkey_blue.json diff --git a/tests/apps/resources/btc_getpubkey_blue.png b/tests/python/apps/resources/btc_getpubkey_blue.png similarity index 100% rename from tests/apps/resources/btc_getpubkey_blue.png rename to tests/python/apps/resources/btc_getpubkey_blue.png diff --git a/tests/apps/resources/btc_getpubkey_nanos.json b/tests/python/apps/resources/btc_getpubkey_nanos.json similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanos.json rename to tests/python/apps/resources/btc_getpubkey_nanos.json diff --git a/tests/apps/resources/btc_getpubkey_nanos.png b/tests/python/apps/resources/btc_getpubkey_nanos.png similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanos.png rename to tests/python/apps/resources/btc_getpubkey_nanos.png diff --git a/tests/apps/resources/btc_getpubkey_nanosp.json b/tests/python/apps/resources/btc_getpubkey_nanosp.json similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanosp.json rename to tests/python/apps/resources/btc_getpubkey_nanosp.json diff --git a/tests/apps/resources/btc_getpubkey_nanosp.png b/tests/python/apps/resources/btc_getpubkey_nanosp.png similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanosp.png rename to tests/python/apps/resources/btc_getpubkey_nanosp.png diff --git a/tests/apps/resources/btc_getpubkey_nanox.json b/tests/python/apps/resources/btc_getpubkey_nanox.json similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanox.json rename to tests/python/apps/resources/btc_getpubkey_nanox.json diff --git a/tests/apps/resources/btc_getpubkey_nanox.png b/tests/python/apps/resources/btc_getpubkey_nanox.png similarity index 100% rename from tests/apps/resources/btc_getpubkey_nanox.png rename to tests/python/apps/resources/btc_getpubkey_nanox.png diff --git a/tests/apps/test_btc.py b/tests/python/apps/test_btc.py similarity index 100% rename from tests/apps/test_btc.py rename to tests/python/apps/test_btc.py diff --git a/tests/apps/test_btc_testnet.py b/tests/python/apps/test_btc_testnet.py similarity index 100% rename from tests/apps/test_btc_testnet.py rename to tests/python/apps/test_btc_testnet.py diff --git a/tests/apps/test_ram_page.py b/tests/python/apps/test_ram_page.py similarity index 100% rename from tests/apps/test_ram_page.py rename to tests/python/apps/test_ram_page.py diff --git a/tests/apps/test_vnc.py b/tests/python/apps/test_vnc.py similarity index 100% rename from tests/apps/test_vnc.py rename to tests/python/apps/test_vnc.py diff --git a/tests/apps/conftest.py b/tests/python/conftest.py similarity index 67% rename from tests/apps/conftest.py rename to tests/python/conftest.py index 196321e7..0c308da5 100644 --- a/tests/apps/conftest.py +++ b/tests/python/conftest.py @@ -2,20 +2,21 @@ import os import re from collections import namedtuple +from pathlib import Path from typing import List from speculos.client import SpeculosClient -SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +# going back from...(conftest.py \ python \ tests \ git root) / 'apps' +APP_DIR = Path(__file__).resolve().parent. parent. parent / "apps" AppInfo = namedtuple("AppInfo", ["filepath", "model", "name", "sdk", "hash"]) -def app_info_from_path(path) -> AppInfo: +def app_info_from_path(path: Path) -> AppInfo: # name example: nanos#btc#1.5#5b6693b8.elf app_regexp = re.compile(r"^(nanos|nanox|blue|nanosp)#([^#]+)#([^#][\d\w\-.]+)#([a-f0-9]*)\.elf$") - filename = os.path.basename(path) - matching = re.match(app_regexp, filename) + matching = re.match(app_regexp, path.name) if not matching: return None assert len(matching.groups()) == 4 @@ -23,7 +24,7 @@ def app_info_from_path(path) -> AppInfo: hash=matching.group(4)) -def list_apps_to_test(app_dir) -> List[AppInfo]: +def list_apps_to_test() -> List[AppInfo]: """ List apps matching the pattern: @@ -37,11 +38,10 @@ def list_apps_to_test(app_dir) -> List[AppInfo]: 'apps/nanos#btc#1.5#5b6693b8.elf' """ all_apps = [] - for filename in os.listdir(app_dir): - if "#" not in filename: + for appfile in APP_DIR.iterdir(): + if "#" not in appfile.name: continue - path = os.path.join(app_dir, filename) - info = app_info_from_path(path) + info = app_info_from_path(appfile) if not info: pytest.fail( f"An unexpected file was found in apps/, with a # but not matching the pattern: {filename!r}" @@ -53,25 +53,21 @@ def list_apps_to_test(app_dir) -> List[AppInfo]: @pytest.fixture(scope="function") def app(request, client): - return app_info_from_path(client.app) + return app_info_from_path(Path(client.app)) -def get_apps(name): - """Retrieve the list of apps in the ../apps directory.""" - app_dir = os.path.join(SCRIPT_DIR, os.pardir, os.pardir, "apps") - apps = list_apps_to_test(app_dir) - apps = [app for app in apps if app.name == name] - return apps +def get_apps(name: str) -> List[AppInfo]: + """Retrieve the list of apps in the ../../apps directory.""" + return [app for app in list_apps_to_test() if app.name == name] -def default_btc_app(): - app_dir = os.path.join(SCRIPT_DIR, os.pardir, os.pardir, "apps") - filepath = os.path.realpath(os.path.join(app_dir, "btc.elf")) +def default_btc_app() -> List[AppInfo]: + filepath = (APP_DIR / "btc.elf").resolve() apps = get_apps("btc") - return [app for app in apps if os.path.realpath(app.filepath) == filepath] + return [app for app in apps if app.filepath == filepath] -def idfn(app): +def idfn(app: Path) -> str: """ Set the test ID to the app file name for each test running on a set of apps. @@ -80,14 +76,14 @@ def idfn(app): These IDs can be used with -k to select specific cases to run, and they will also identify the specific case when one is failing. """ - return os.path.basename(app.filepath) + return app.filepath def client_instance(app, additional_args=None): args = ["--model", app.model, "--sdk", app.sdk] if additional_args is not None: args += additional_args - return SpeculosClient(app.filepath, args=args) + return SpeculosClient(str(app.filepath), args=args) @pytest.fixture(scope="module", params=get_apps("btc"), ids=idfn) @@ -99,9 +95,9 @@ def client_btc(request): @pytest.fixture(scope="module", params=get_apps("btc-test"), ids=idfn) def client_btc_testnet(request): app = request.param - btc_app = app.filepath.replace("btc-test", "btc") - assert os.path.exists(btc_app) - args = ["-l", "Bitcoin:%s" % btc_app] + btc_app = app.filepath.parent / app.filepath.name.replace("btc-test", "btc") + assert btc_app.is_file() + args = ["-l", "Bitcoin:%s" % str(btc_app)] with client_instance(request.param, additional_args=args) as _client: yield _client @@ -123,3 +119,13 @@ def client_vnc(request): args = list(get_closest_marker("additional_args").args) with client_instance(request.param, args) as _client: yield _client + + +@pytest.fixture(scope="class") +def client(request): + """Run the API tests on the default btc.elf app.""" + + info = app_info_from_path((APP_DIR / "btc.elf").resolve()) + args = ["--model", info.model, "--sdk", info.sdk] + with SpeculosClient(app=str(info.filepath), args=args) as _client: + yield _client diff --git a/tests/mcu/resources/automation_invalid_action_args.json b/tests/python/mcu/resources/automation_invalid_action_args.json similarity index 100% rename from tests/mcu/resources/automation_invalid_action_args.json rename to tests/python/mcu/resources/automation_invalid_action_args.json diff --git a/tests/mcu/resources/automation_invalid_action_name.json b/tests/python/mcu/resources/automation_invalid_action_name.json similarity index 100% rename from tests/mcu/resources/automation_invalid_action_name.json rename to tests/python/mcu/resources/automation_invalid_action_name.json diff --git a/tests/mcu/resources/automation_invalid_rule_key.json b/tests/python/mcu/resources/automation_invalid_rule_key.json similarity index 100% rename from tests/mcu/resources/automation_invalid_rule_key.json rename to tests/python/mcu/resources/automation_invalid_rule_key.json diff --git a/tests/mcu/resources/automation_valid.json b/tests/python/mcu/resources/automation_valid.json similarity index 100% rename from tests/mcu/resources/automation_valid.json rename to tests/python/mcu/resources/automation_valid.json diff --git a/tests/mcu/test_automation.py b/tests/python/mcu/test_automation.py similarity index 100% rename from tests/mcu/test_automation.py rename to tests/python/mcu/test_automation.py diff --git a/tests/pytest.ini b/tests/python/pytest.ini similarity index 100% rename from tests/pytest.ini rename to tests/python/pytest.ini diff --git a/tests/unit/__init__.py b/tests/python/unit/__init__.py similarity index 100% rename from tests/unit/__init__.py rename to tests/python/unit/__init__.py diff --git a/tests/unit/test_client_Api.py b/tests/python/unit/test_client_Api.py similarity index 100% rename from tests/unit/test_client_Api.py rename to tests/python/unit/test_client_Api.py diff --git a/tests/unit/test_client_SpeculosClient.py b/tests/python/unit/test_client_SpeculosClient.py similarity index 100% rename from tests/unit/test_client_SpeculosClient.py rename to tests/python/unit/test_client_SpeculosClient.py diff --git a/tests/syscalls/CMakeLists.txt b/tests/syscalls/CMakeLists.txt deleted file mode 100644 index 700745a3..00000000 --- a/tests/syscalls/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -add_definitions(-DST31) - -link_libraries(emu -lcmocka-static) - -add_executable(hello hello.c) - -add_executable(test_aes test_aes.c nist_cavp.c utils.c) -add_executable(test_bip32 test_bip32.c utils.c) -add_executable(test_blake2 test_blake2.c nist_cavp.c utils.c) -add_executable(test_bls test_bls.c utils.c) -add_executable(test_crc16 test_crc16.c) -add_executable(test_ecdh test_ecdh.c utils.c) -add_executable(test_ecdsa test_ecdsa.c utils.c) -add_executable(test_ec test_ec.c utils.c) -add_executable(test_ecpoint test_ecpoint.c utils.c) -add_executable(test_eddsa test_eddsa.c utils.c) -add_executable(test_eip2333 test_eip2333.c utils.c) -add_executable(test_endorsement test_endorsement.c) -add_executable(test_hmac test_hmac.c utils.c) -add_executable(test_math test_math.c) -add_executable(test_mpi_rng test_mpi_rng.c utils.c) -add_executable(test_os_global_pin_is_validated test_os_global_pin_is_validated.c) -add_executable(test_rfc6979 test_rfc6979.c utils.c) -add_executable(test_ripemd test_ripemd.c utils.c) -add_executable(test_sha2 test_sha2.c nist_cavp.c utils.c) -add_executable(test_sha3 test_sha3.c nist_cavp.c utils.c) -add_executable(test_slip21 test_slip21.c) - -foreach(target hello test_aes test_bip32 test_blake2 test_bls test_crc16 test_ec test_ecpoint test_ecdh test_ecdsa test_eddsa test_endorsement test_hmac test_math test_os_global_pin_is_validated test_rfc6979 test_ripemd test_sha2 test_sha3 test_slip21 test_eip2333) - add_test(NAME ${target} COMMAND qemu-arm-static ${target} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -endforeach()