1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-14 16:42:02 +00:00

temp: thp rest

[no changelog]
This commit is contained in:
M1nd3r 2025-02-04 15:23:50 +01:00
parent 0620c531eb
commit b4dd6b7005
85 changed files with 12061 additions and 990 deletions

View File

@ -49,7 +49,7 @@ jobs:
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
core_firmware: core_firmware:
name: Build firmware name: Build firmware (${{ matrix.model }}, ${{ matrix.coins }}, ${{ matrix.type }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@ -57,10 +57,27 @@ jobs:
model: [T2T1, T3B1, T3T1, T3W1] model: [T2T1, T3B1, T3T1, T3W1]
coins: [universal, btconly] coins: [universal, btconly]
type: ${{ fromJSON(github.event_name == 'schedule' && '["normal", "debuglink", "production"]' || '["normal", "debuglink"]') }} type: ${{ fromJSON(github.event_name == 'schedule' && '["normal", "debuglink", "production"]' || '["normal", "debuglink"]') }}
protocol: [v1]
include: include:
- model: D001 - model: D001
coins: universal coins: universal
type: normal type: normal
- model: T2T1
coins: universal
type: debuglink
protocol: v2
- model: T2T1
coins: btconly
type: debuglink
protocol: v2
- model: T3T1
coins: universal
type: debuglink
protocol: v2
- model: T3T1
coins: btconly
type: debuglink
protocol: v2
exclude: exclude:
- model: T3W1 - model: T3W1
type: production type: production
@ -70,6 +87,7 @@ jobs:
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }} PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
PRODUCTION: ${{ matrix.type == 'production' && '1' || '0' }} PRODUCTION: ${{ matrix.type == 'production' && '1' || '0' }}
BOOTLOADER_DEVEL: ${{ matrix.model == 'T3W1' && '1' || '0' }} BOOTLOADER_DEVEL: ${{ matrix.model == 'T3W1' && '1' || '0' }}
THP: ${{ matrix.protocol == 'v2' && '1' || '0'}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -90,7 +108,7 @@ jobs:
if: matrix.coins == 'btconly' && matrix.type != 'debuglink' if: matrix.coins == 'btconly' && matrix.type != 'debuglink'
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-firmware-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.type }} name: core-firmware-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.type }}-protocol_${{ matrix.protocol }}
path: | path: |
core/build/boardloader/*.bin core/build/boardloader/*.bin
core/build/bootloader/*.bin core/build/bootloader/*.bin
@ -101,7 +119,7 @@ jobs:
retention-days: 7 retention-days: 7
core_emu: core_emu:
name: Build emu name: Build emu (${{ matrix.model }}, ${{ matrix.coins }}, ${{ matrix.type }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: param needs: param
strategy: strategy:
@ -112,15 +130,38 @@ jobs:
# type: [normal, debuglink] # type: [normal, debuglink]
type: [debuglink] type: [debuglink]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1]
exclude: exclude:
- type: normal - type: normal
asan: asan asan: asan
include:
- model: T2T1
coins: universal
type: debuglink
asan: noasan
protocol: v2
- model: T2T1
coins: btconly
type: debuglink
asan: noasan
protocol: v2
- model: T3T1
coins: universal
type: debuglink
asan: noasan
protocol: v2
- model: T3T1
coins: btconly
type: debuglink
asan: noasan
protocol: v2
env: env:
TREZOR_MODEL: ${{ matrix.model }} TREZOR_MODEL: ${{ matrix.model }}
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }} BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }} PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }} ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt" LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
THP: ${{ matrix.protocol == 'v2' && '1' || '0'}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -132,7 +173,7 @@ jobs:
- run: cp core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-${{ matrix.model }}-${{ matrix.coins }} - run: cp core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-${{ matrix.model }}-${{ matrix.coins }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.type }}-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.type }}-${{ matrix.asan }}-protocol_${{ matrix.protocol }}
path: | path: |
core/build/unix/trezor-emu-core* core/build/unix/trezor-emu-core*
core/build/bootloader_emu/bootloader.elf core/build/bootloader_emu/bootloader.elf
@ -177,7 +218,7 @@ jobs:
retention-days: 2 retention-days: 2
core_unit_python_test: core_unit_python_test:
name: Python unit tests name: Python unit tests (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: param needs: param
strategy: strategy:
@ -185,10 +226,12 @@ jobs:
matrix: matrix:
model: [T2T1, T3B1, T3T1, T3W1] model: [T2T1, T3B1, T3T1, T3W1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1, v2]
env: env:
TREZOR_MODEL: ${{ matrix.model }} TREZOR_MODEL: ${{ matrix.model }}
ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }} ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt" LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
THP: ${{ matrix.protocol == 'v2' && '1' || '0'}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -198,7 +241,7 @@ jobs:
- run: nix-shell --run "poetry run make -C core test" - run: nix-shell --run "poetry run make -C core test"
core_unit_rust_test: core_unit_rust_test:
name: Rust unit tests name: Rust unit tests (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -208,12 +251,14 @@ jobs:
matrix: matrix:
model: [T2T1, T3B1, T3T1] model: [T2T1, T3B1, T3T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1, v2]
env: env:
TREZOR_MODEL: ${{ matrix.model }} TREZOR_MODEL: ${{ matrix.model }}
ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }} ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }}
RUSTC_BOOTSTRAP: ${{ matrix.asan == 'asan' && '1' || '0' }} RUSTC_BOOTSTRAP: ${{ matrix.asan == 'asan' && '1' || '0' }}
RUSTFLAGS: ${{ matrix.asan == 'asan' && '-Z sanitizer=address' || '' }} RUSTFLAGS: ${{ matrix.asan == 'asan' && '-Z sanitizer=address' || '' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt" LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
THP: ${{ matrix.protocol == 'v2' && '1' || '0'}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -237,7 +282,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-noasan name: core-emu-${{ matrix.model }}-universal-debuglink-noasan-protocol_v1
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -248,7 +293,7 @@ jobs:
# See artifacts for a comprehensive report of UI. # See artifacts for a comprehensive report of UI.
# See [docs/tests/ui-tests](../tests/ui-tests.md) for more info. # See [docs/tests/ui-tests](../tests/ui-tests.md) for more info.
core_device_test: core_device_test:
name: Device tests name: Device tests (${{ matrix.model }}, ${{ matrix.coins }}, ${{ matrix.asan }}, ${{ matrix.lang }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -260,6 +305,13 @@ jobs:
coins: [universal, btconly] coins: [universal, btconly]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
lang: ${{ fromJSON(needs.param.outputs.test_lang) }} lang: ${{ fromJSON(needs.param.outputs.test_lang) }}
protocol: [v1]
include:
- model: T2T1
coins: universal
asan: noasan
lang: en
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
TREZOR_MODEL: ${{ matrix.model }} TREZOR_MODEL: ${{ matrix.model }}
@ -268,13 +320,14 @@ jobs:
PYTEST_TIMEOUT: ${{ matrix.asan == 'asan' && 600 || 400 }} PYTEST_TIMEOUT: ${{ matrix.asan == 'asan' && 600 || 400 }}
ACTIONS_DO_UI_TEST: ${{ matrix.coins == 'universal' && matrix.asan == 'noasan' }} ACTIONS_DO_UI_TEST: ${{ matrix.coins == 'universal' && matrix.asan == 'noasan' }}
TEST_LANG: ${{ matrix.lang }} TEST_LANG: ${{ matrix.lang }}
THP: ${{ matrix.protocol == 'v2' && '1' || '0'}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-${{ matrix.coins }}-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-${{ matrix.coins }}-debuglink-${{ matrix.asan }}-protocol_${{ matrix.protocol }}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -283,7 +336,7 @@ jobs:
if: failure() if: failure()
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-test-device-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.lang }}-${{ matrix.asan }} name: core-test-device-${{ matrix.model }}-${{ matrix.coins }}-${{ matrix.lang }}-${{ matrix.asan }}-protocol_${{ matrix.protocol }}
path: tests/trezor.log path: tests/trezor.log
retention-days: 7 retention-days: 7
if: always() if: always()
@ -299,7 +352,7 @@ jobs:
# Click tests - UI. # Click tests - UI.
# See [docs/tests/click-tests](../tests/click-tests.md) for more info. # See [docs/tests/click-tests](../tests/click-tests.md) for more info.
core_click_test: core_click_test:
name: Click tests name: Click tests (${{ matrix.model }}, ${{ matrix.asan }}, ${{ matrix.lang }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -311,6 +364,12 @@ jobs:
model: [T2T1, T3B1, T3T1] model: [T2T1, T3B1, T3T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
lang: ${{ fromJSON(needs.param.outputs.test_lang) }} lang: ${{ fromJSON(needs.param.outputs.test_lang) }}
protocol: [v1]
include:
- model: T2T1
asan: noasan
lang: en
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
# MULTICORE: 4 # more could interfere with other jobs # MULTICORE: 4 # more could interfere with other jobs
@ -322,7 +381,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -332,7 +391,7 @@ jobs:
if: ${{ matrix.asan == 'asan' }} if: ${{ matrix.asan == 'asan' }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-test-click-${{ matrix.model }}-${{ matrix.lang }}-${{ matrix.asan }} name: core-test-click-${{ matrix.model }}-${{ matrix.lang }}-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: tests/trezor.log path: tests/trezor.log
retention-days: 7 retention-days: 7
if: always() if: always()
@ -349,7 +408,7 @@ jobs:
# Upgrade tests. # Upgrade tests.
# See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info. # See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info.
core_upgrade_test: core_upgrade_test:
name: Upgrade tests name: Upgrade tests (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -361,6 +420,7 @@ jobs:
# FIXME: T3T1 https://github.com/trezor/trezor-firmware/issues/3595 # FIXME: T3T1 https://github.com/trezor/trezor-firmware/issues/3595
model: [T2T1] model: [T2T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1, v2]
env: env:
TREZOR_UPGRADE_TEST: core TREZOR_UPGRADE_TEST: core
PYTEST_TIMEOUT: 400 PYTEST_TIMEOUT: 400
@ -370,7 +430,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -380,7 +440,7 @@ jobs:
# Persistence tests - UI. # Persistence tests - UI.
core_persistence_test: core_persistence_test:
name: Persistence tests name: Persistence tests (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -391,6 +451,11 @@ jobs:
matrix: matrix:
model: [T2T1, T3B1, T3T1] model: [T2T1, T3B1, T3T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1]
include:
- model: T2T1
asan: noasan
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
PYTEST_TIMEOUT: 400 PYTEST_TIMEOUT: 400
@ -400,7 +465,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -433,7 +498,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-noasan name: core-emu-${{ matrix.model }}-universal-debuglink-noasan-protocol_v1
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment # XXX poetry maybe not needed - uses: ./.github/actions/environment # XXX poetry maybe not needed
@ -491,7 +556,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-firmware-${{ matrix.model }}-universal-normal # FIXME: s/normal/debuglink/ name: core-firmware-${{ matrix.model }}-universal-normal-protocol_v1 # FIXME: s/normal/debuglink/
path: core/build path: core/build
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
- run: nix-shell --run "poetry run core/tools/size/checker.py core/build/firmware/firmware.elf" - run: nix-shell --run "poetry run core/tools/size/checker.py core/build/firmware/firmware.elf"
@ -515,7 +580,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-firmware-${{ matrix.model }}-universal-normal name: core-firmware-${{ matrix.model }}-universal-normal-protocol_v1
path: core/build path: core/build
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
- run: nix-shell --run "poetry run core/tools/size/compare_master.py core/build/firmware/firmware.elf -r firmware_elf_size_report.txt" - run: nix-shell --run "poetry run core/tools/size/compare_master.py core/build/firmware/firmware.elf -r firmware_elf_size_report.txt"
@ -527,7 +592,7 @@ jobs:
# Monero tests. # Monero tests.
core_monero_test: core_monero_test:
name: Monero test name: Monero test (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -537,6 +602,11 @@ jobs:
matrix: matrix:
model: [T2T1, T3B1, T3T1] model: [T2T1, T3B1, T3T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1]
include:
- model: T2T1
asan: noasan
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
PYTEST_TIMEOUT: 400 PYTEST_TIMEOUT: 400
@ -546,7 +616,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: cachix/install-nix-action@v23 - uses: cachix/install-nix-action@v23
@ -557,7 +627,7 @@ jobs:
- run: nix-shell --arg fullDeps true --run "unset _PYTHON_SYSCONFIGDATA_NAME && poetry run make -C core test_emu_monero" - run: nix-shell --arg fullDeps true --run "unset _PYTHON_SYSCONFIGDATA_NAME && poetry run make -C core test_emu_monero"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-test-monero-${{ matrix.model }}-${{ matrix.asan }} name: core-test-monero-${{ matrix.model }}-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: | path: |
tests/trezor.log tests/trezor.log
core/tests/trezor_monero_tests.log core/tests/trezor_monero_tests.log
@ -568,7 +638,7 @@ jobs:
# Tests for U2F and HID. # Tests for U2F and HID.
core_u2f_test: core_u2f_test:
name: U2F test name: U2F test (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -578,6 +648,11 @@ jobs:
matrix: matrix:
model: [T2T1, T3B1, T3T1] model: [T2T1, T3B1, T3T1]
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1]
include:
- model: T2T1
asan: noasan
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
PYTEST_TIMEOUT: 400 PYTEST_TIMEOUT: 400
@ -587,7 +662,7 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
@ -595,7 +670,7 @@ jobs:
- run: nix-shell --run "poetry run make -C core test_emu_u2f" - run: nix-shell --run "poetry run make -C core test_emu_u2f"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-test-u2f-${{ matrix.model }}-${{ matrix.asan }} name: core-test-u2f-${{ matrix.model }}-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: tests/trezor.log path: tests/trezor.log
retention-days: 7 retention-days: 7
if: always() if: always()
@ -603,7 +678,7 @@ jobs:
# FIDO2 device tests. # FIDO2 device tests.
core_fido2_test: core_fido2_test:
name: FIDO2 test name: FIDO2 test (${{ matrix.model }}, ${{ matrix.asan }}${{ matrix.protocol=='v2' && ', THP' || ''}})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- param - param
@ -613,6 +688,11 @@ jobs:
matrix: matrix:
model: [T2T1, T3T1] # XXX T3B1 https://github.com/trezor/trezor-firmware/issues/2724 model: [T2T1, T3T1] # XXX T3B1 https://github.com/trezor/trezor-firmware/issues/2724
asan: ${{ fromJSON(needs.param.outputs.asan) }} asan: ${{ fromJSON(needs.param.outputs.asan) }}
protocol: [v1]
include:
- model: T2T1
asan: noasan
protocol: v2
env: env:
TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }} TREZOR_PROFILING: ${{ matrix.asan == 'noasan' && '1' || '0' }}
PYTEST_TIMEOUT: 400 PYTEST_TIMEOUT: 400
@ -622,14 +702,14 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }} name: core-emu-${{ matrix.model }}-universal-debuglink-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: core/build path: core/build
- run: chmod +x core/build/unix/trezor-emu-core* - run: chmod +x core/build/unix/trezor-emu-core*
- uses: ./.github/actions/environment - uses: ./.github/actions/environment
- run: nix-shell --run "poetry run make -C core test_emu_fido2" - run: nix-shell --run "poetry run make -C core test_emu_fido2"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: core-test-fido2-${{ matrix.model }}-${{ matrix.asan }} name: core-test-fido2-${{ matrix.model }}-${{ matrix.asan }}-protocol_${{matrix.protocol}}
path: | path: |
tests/trezor.log tests/trezor.log
retention-days: 7 retention-days: 7
@ -728,7 +808,7 @@ jobs:
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
pattern: core-emu*debuglink-noasan pattern: core-emu*debuglink-noasan-protocol_v*
merge-multiple: true merge-multiple: true
- name: Configure aws credentials - name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v4
@ -751,7 +831,7 @@ jobs:
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
pattern: core-emu*debuglink-noasan pattern: core-emu*debuglink-noasan-protocol_v*
merge-multiple: true merge-multiple: true
- name: Configure aws credentials - name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v4

View File

@ -39,6 +39,10 @@ message Failure {
Failure_PinMismatch = 12; Failure_PinMismatch = 12;
Failure_WipeCodeMismatch = 13; Failure_WipeCodeMismatch = 13;
Failure_InvalidSession = 14; Failure_InvalidSession = 14;
Failure_ThpUnallocatedSession = 15;
Failure_InvalidProtocol = 16;
Failure_BufferError = 17;
Failure_DeviceIsBusy = 18;
Failure_FirmwareError = 99; Failure_FirmwareError = 99;
} }
} }

View File

@ -85,7 +85,7 @@ message DebugLinkRecordScreen {
} }
/** /**
* Request: Computer asks for device state * Request: Host asks for device state
* @start * @start
* @next DebugLinkState * @next DebugLinkState
*/ */
@ -134,6 +134,29 @@ message DebugLinkState {
repeated string tokens = 13; // current layout represented as a list of string tokens repeated string tokens = 13; // current layout represented as a list of string tokens
} }
/**
* Request: Host asks for device pairing info
* @start
* @next DebugLinkPairingInfo
*/
message DebugLinkGetPairingInfo {
optional bytes channel_id = 1; // ID of the THP channel to get pairing info from
optional bytes handshake_hash = 2; // handshake hash of the THP channel
optional bytes nfc_secret_host = 3; // host's NFC secret (In case of NFC pairing)
}
/**
* Response: Device pairing info
* @end
*/
message DebugLinkPairingInfo {
optional bytes channel_id = 1; // ID of the THP channel the pairing info is from
optional bytes handshake_hash = 2; // handshake hash of the THP channel
optional uint32 code_entry_code = 3; // CodeEntry pairing code
optional bytes code_qr_code = 4; // QrCode pairing code
optional bytes nfc_secret_trezor = 5; // NFC secret used in NFC pairing
}
/** /**
* Request: Ask device to restart * Request: Ask device to restart
* @start * @start

View File

@ -9,6 +9,224 @@ import "options.proto";
option (include_in_bitcoin_only) = true; option (include_in_bitcoin_only) = true;
/**
* Mapping between Trezor wire identifier (uint) and a Thp protobuf message
*/
enum ThpMessageType {
reserved 0 to 999; // Values reserved by other messages, see messages.proto
ThpMessageType_ThpCreateNewSession = 1000 [(bitcoin_only)=true];
ThpMessageType_ThpPairingRequest = 1006 [(bitcoin_only) = true];
ThpMessageType_ThpPairingRequestApproved = 1007 [(bitcoin_only) = true];
ThpMessageType_ThpSelectMethod = 1008 [(bitcoin_only) = true];
ThpMessageType_ThpPairingPreparationsFinished = 1009 [(bitcoin_only) = true];
ThpMessageType_ThpCredentialRequest = 1010 [(bitcoin_only) = true];
ThpMessageType_ThpCredentialResponse = 1011 [(bitcoin_only) = true];
ThpMessageType_ThpEndRequest = 1012 [(bitcoin_only) = true];
ThpMessageType_ThpEndResponse = 1013 [(bitcoin_only) = true];
ThpMessageType_ThpCodeEntryCommitment = 1016 [(bitcoin_only)=true];
ThpMessageType_ThpCodeEntryChallenge = 1017 [(bitcoin_only)=true];
ThpMessageType_ThpCodeEntryCpaceTrezor = 1018 [(bitcoin_only)=true];
ThpMessageType_ThpCodeEntryCpaceHostTag = 1019 [(bitcoin_only)=true];
ThpMessageType_ThpCodeEntrySecret = 1020 [(bitcoin_only)=true];
ThpMessageType_ThpQrCodeTag = 1024 [(bitcoin_only)=true];
ThpMessageType_ThpQrCodeSecret = 1025 [(bitcoin_only)=true];
ThpMessageType_ThpNfcTagHost = 1032 [(bitcoin_only)=true];
ThpMessageType_ThpNfcTagTrezor = 1033 [(bitcoin_only)=true];
reserved 1100 to 2147483647; // Values reserved by other messages, see messages.proto
}
/**
* Numeric identifiers of pairing methods.
* @embed
*/
enum ThpPairingMethod {
SkipPairing = 1; // Trust without MITM protection.
CodeEntry = 2; // User types code diplayed on Trezor into the host application.
QrCode = 3; // User scans code displayed on Trezor into host application.
NFC = 4; // Trezor and host application exchange authentication secrets via NFC.
}
/**
* @embed
*/
message ThpDeviceProperties {
optional string internal_model = 1; // Internal model name e.g. "T2B1".
optional uint32 model_variant = 2; // Encodes the device properties such as color.
optional uint32 protocol_version_major = 3; // The major version of the communication protocol used by the firmware.
optional uint32 protocol_version_minor = 4; // The minor version of the communication protocol used by the firmware.
repeated ThpPairingMethod pairing_methods = 5; // The pairing methods supported by the Trezor.
}
/**
* @embed
*/
message ThpHandshakeCompletionReqNoisePayload {
optional bytes host_pairing_credential = 1; // Host's pairing credential
}
/**
* Request: Ask device for a new session with given passphrase.
* @start
* @next Success
*/
message ThpCreateNewSession{
optional string passphrase = 1;
optional bool on_device = 2; // User wants to enter passphrase on the device
optional bool derive_cardano = 3; // If True, Cardano keys will be derived. Ignored with BTC-only
}
/**
* Request: Start pairing process.
* @start
* @next ThpPairingRequestApproved
*/
message ThpPairingRequest{
optional string host_name = 1; // Human-readable host name
}
/**
* Response: Host is allowed to start pairing process.
* @start
* @next ThpSelectMethod
*/
message ThpPairingRequestApproved{
}
/**
* Request: Start pairing using the method selected.
* @start
* @next ThpPairingPreparationsFinished
* @next ThpCodeEntryCommitment
*/
message ThpSelectMethod {
optional ThpPairingMethod selected_pairing_method = 1;
}
/**
* Response: Pairing is ready for user input / OOB communication.
* @next ThpCodeEntryCpace
* @next ThpQrCodeTag
* @next ThpNfcTagHost
*/
message ThpPairingPreparationsFinished{
}
/**
* Response: If Code Entry is an allowed pairing option, Trezor responds with a commitment.
* @next ThpCodeEntryChallenge
*/
message ThpCodeEntryCommitment {
optional bytes commitment = 1; // SHA-256 of Trezor's random 32-byte secret
}
/**
* Response: Host responds to Trezor's Code Entry commitment with a challenge.
* @next ThpCodeEntryCpaceTrezor
*/
message ThpCodeEntryChallenge {
optional bytes challenge = 1; // Host's random 32-byte challenge
}
/**
* Response: Trezor continues with the CPACE protocol.
* @next ThpCodeEntryCpaceHostTag
*/
message ThpCodeEntryCpaceTrezor {
optional bytes cpace_trezor_public_key = 1; // Trezor's ephemeral CPace public key
}
/**
* Request: User selected Code Entry option in Host. Host starts CPACE protocol with Trezor.
* @next ThpCodeEntrySecret
*/
message ThpCodeEntryCpaceHostTag {
optional bytes cpace_host_public_key = 1; // Host's ephemeral CPace public key
optional bytes tag = 2; // SHA-256 of shared secret
}
/**
* Response: Trezor finishes the CPACE protocol.
* @next ThpCredentialRequest
* @next ThpEndRequest
*/
message ThpCodeEntrySecret {
optional bytes secret = 1; // Trezor's secret
}
/**
* Request: User selected QR Code pairing option. Host sends a QR Tag.
* @next ThpQrCodeSecret
*/
message ThpQrCodeTag {
optional bytes tag = 1; // SHA-256 of shared secret
}
/**
* Response: Trezor sends the QR secret.
* @next ThpCredentialRequest
* @next ThpEndRequest
*/
message ThpQrCodeSecret {
optional bytes secret = 1; // Trezor's secret
}
/**
* Request: User selected Unidirectional NFC pairing option. Host sends an Unidirectional NFC Tag.
* @next ThpNfcTagTrezor
*/
message ThpNfcTagHost {
optional bytes tag = 1; // Host's tag
}
/**
* Response: Trezor sends the Unidirectioal NFC secret.
* @next ThpCredentialRequest
* @next ThpEndRequest
*/
message ThpNfcTagTrezor {
optional bytes tag = 1; // Trezor's tag
}
/**
* Request: Host requests issuance of a new pairing credential.
* @start
* @next ThpCredentialResponse
*/
message ThpCredentialRequest {
optional bytes host_static_pubkey = 1; // Host's static public key used in the handshake.
optional bool autoconnect = 2; // Whether host wants to autoconnect without user confirmation
}
/**
* Response: Trezor issues a new pairing credential.
* @next ThpCredentialRequest
* @next ThpEndRequest
*/
message ThpCredentialResponse {
optional bytes trezor_static_pubkey = 1; // Trezor's static public key used in the handshake.
optional bytes credential = 2; // The pairing credential issued by the Trezor to the host.
}
/**
* Request: Host requests transition to the encrypted traffic phase.
* @start
* @next ThpEndResponse
*/
message ThpEndRequest {}
/**
* Response: Trezor approves transition to the encrypted traffic phase
* @end
*/
message ThpEndResponse {}
/** /**
* Only for internal use. * Only for internal use.
* @embed * @embed
@ -16,6 +234,7 @@ option (include_in_bitcoin_only) = true;
message ThpCredentialMetadata { message ThpCredentialMetadata {
option (internal_only) = true; option (internal_only) = true;
optional string host_name = 1; // Human-readable host name optional string host_name = 1; // Human-readable host name
optional bool autoconnect = 2; // Whether host is allowed to autoconnect without user confirmation
} }
/** /**

View File

@ -134,6 +134,8 @@ enum MessageType {
MessageType_DebugLinkWatchLayout = 9006 [(bitcoin_only) = true, (wire_debug_in) = true]; MessageType_DebugLinkWatchLayout = 9006 [(bitcoin_only) = true, (wire_debug_in) = true];
MessageType_DebugLinkResetDebugEvents = 9007 [(bitcoin_only) = true, (wire_debug_in) = true]; MessageType_DebugLinkResetDebugEvents = 9007 [(bitcoin_only) = true, (wire_debug_in) = true];
MessageType_DebugLinkOptigaSetSecMax = 9008 [(bitcoin_only) = true, (wire_debug_in) = true]; MessageType_DebugLinkOptigaSetSecMax = 9008 [(bitcoin_only) = true, (wire_debug_in) = true];
MessageType_DebugLinkGetPairingInfo = 9009 [(bitcoin_only) = true, (wire_debug_in) = true];
MessageType_DebugLinkPairingInfo = 9010 [(bitcoin_only) = true, (wire_debug_out) = true];
// Ethereum // Ethereum
MessageType_EthereumGetPublicKey = 450 [(wire_in) = true]; MessageType_EthereumGetPublicKey = 450 [(wire_in) = true];

View File

@ -62,6 +62,7 @@ INT_TYPES = (
) )
MESSAGE_TYPE_ENUM = "MessageType" MESSAGE_TYPE_ENUM = "MessageType"
THP_MESSAGE_TYPE_ENUM = "ThpMessageType"
LengthDelimited = c.Struct( LengthDelimited = c.Struct(
"len" / c.VarInt, "len" / c.VarInt,
@ -239,6 +240,9 @@ class ProtoMessage:
@classmethod @classmethod
def from_message(cls, descriptor: "Descriptor", message): def from_message(cls, descriptor: "Descriptor", message):
message_type = find_by_name(descriptor.message_type_enum.value, message.name) message_type = find_by_name(descriptor.message_type_enum.value, message.name)
thp_message_type = None
if not isinstance(descriptor.thp_message_type_enum,tuple):
thp_message_type = find_by_name(descriptor.thp_message_type_enum.value, message.name)
# use extensions set on the message_type entry (if any) # use extensions set on the message_type entry (if any)
extensions = descriptor.get_extensions(message_type) extensions = descriptor.get_extensions(message_type)
# override with extensions set on the message itself # override with extensions set on the message itself
@ -248,6 +252,8 @@ class ProtoMessage:
wire_type = extensions["wire_type"] wire_type = extensions["wire_type"]
elif message_type is not None: elif message_type is not None:
wire_type = message_type.number wire_type = message_type.number
elif thp_message_type is not None:
wire_type = thp_message_type.number
else: else:
wire_type = None wire_type = None
@ -351,10 +357,13 @@ class Descriptor:
] ]
logging.debug(f"found {len(self.files)} bitcoin-only files") logging.debug(f"found {len(self.files)} bitcoin-only files")
# find message_type enum # find message_type and thp_message_type enum
top_level_enums = itertools.chain.from_iterable(f.enum_type for f in self.files) top_level_enums = itertools.chain.from_iterable(f.enum_type for f in self.files)
self.message_type_enum = find_by_name(top_level_enums, MESSAGE_TYPE_ENUM, ()) self.message_type_enum = find_by_name(top_level_enums, MESSAGE_TYPE_ENUM, ())
top_level_enums = itertools.chain.from_iterable(f.enum_type for f in self.files)
self.thp_message_type_enum = find_by_name(top_level_enums, THP_MESSAGE_TYPE_ENUM, ())
self.convert_enum_value_names(self.message_type_enum) self.convert_enum_value_names(self.message_type_enum)
self.convert_enum_value_names(self.thp_message_type_enum)
# find messages and enums # find messages and enums
self.messages = [] self.messages = []
@ -423,6 +432,8 @@ class Descriptor:
self._nested_types_from_message(nested.orig) self._nested_types_from_message(nested.orig)
def convert_enum_value_names(self, enum): def convert_enum_value_names(self, enum):
if isinstance(enum,tuple):
return
for value in enum.value: for value in enum.value:
value.name = strip_enum_prefix(enum.name, value.name) value.name = strip_enum_prefix(enum.name, value.name)
@ -558,6 +569,8 @@ class RustBlobRenderer:
enums = [] enums = []
cursor = 0 cursor = 0
for enum in sorted(self.descriptor.enums, key=lambda e: e.name): for enum in sorted(self.descriptor.enums, key=lambda e: e.name):
if enum.name == "MessageType":
continue
self.enum_map[enum.name] = cursor self.enum_map[enum.name] = cursor
enum_blob = ENUM_ENTRY.build(sorted(v.number for v in enum.value)) enum_blob = ENUM_ENTRY.build(sorted(v.number for v in enum.value))
enums.append(enum_blob) enums.append(enum_blob)

View File

@ -296,6 +296,10 @@ build_unix: templates ## build unix port
build_unix_frozen: templates build_cross ## build unix port with frozen modules build_unix_frozen: templates build_cross ## build unix port with frozen modules
$(SCONS) $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_EMULATOR_FROZEN=1 $(SCONS) $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_EMULATOR_FROZEN=1
build_unix_frozen_debug: templates build_cross ## build unix port with frozen modules and DEBUG (PYOPT="0")
$(SCONS) $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_EMULATOR_FROZEN=1 \
PYOPT=0
build_unix_debug: templates ## build unix port build_unix_debug: templates ## build unix port
$(SCONS) --max-drift=1 $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \ $(SCONS) --max-drift=1 $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \
TREZOR_EMULATOR_ASAN=1 TREZOR_EMULATOR_DEBUGGABLE=1 TREZOR_EMULATOR_ASAN=1 TREZOR_EMULATOR_DEBUGGABLE=1

View File

@ -578,14 +578,23 @@ if FROZEN:
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) if THP:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/codec/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/thp/*.py'))
if not THP or PYOPT == '0':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/codec/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if not SDCARD else [] exclude_list = []
)) if 'sd_card' not in FEATURES_AVAILABLE:
exclude_list.append(SOURCE_PY_DIR + 'storage/sd_salt.py')
if THP and PYOPT == '1':
exclude_list.append(SOURCE_PY_DIR + 'storage/cache_codec.py')
if not THP:
exclude_list.append(SOURCE_PY_DIR + 'storage/cache_thp.py')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', exclude=exclude_list))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py',

View File

@ -637,14 +637,23 @@ if FROZEN:
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) if THP:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/codec/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/thp/*.py'))
if not THP or PYOPT == '0':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/codec/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if 'sd_card' not in FEATURES_AVAILABLE else [] exclude_list = []
)) if 'sd_card' not in FEATURES_AVAILABLE:
exclude_list.append(SOURCE_PY_DIR + 'storage/sd_salt.py')
if THP and PYOPT == '1':
exclude_list.append(SOURCE_PY_DIR + 'storage/cache_codec.py')
if not THP:
exclude_list.append(SOURCE_PY_DIR + 'storage/cache_thp.py')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', exclude=exclude_list))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py',

View File

@ -51,6 +51,8 @@ storage.cache_codec
import storage.cache_codec import storage.cache_codec
storage.cache_common storage.cache_common
import storage.cache_common import storage.cache_common
storage.cache_thp
import storage.cache_thp
storage.common storage.common
import storage.common import storage.common
storage.debug storage.debug
@ -419,10 +421,50 @@ apps.workflow_handlers
import apps.workflow_handlers import apps.workflow_handlers
if utils.USE_THP: if utils.USE_THP:
trezor.enums.ThpMessageType
import trezor.enums.ThpMessageType
trezor.enums.ThpPairingMethod
import trezor.enums.ThpPairingMethod
trezor.wire.thp
import trezor.wire.thp
trezor.wire.thp.alternating_bit_protocol
import trezor.wire.thp.alternating_bit_protocol
trezor.wire.thp.channel
import trezor.wire.thp.channel
trezor.wire.thp.channel_manager
import trezor.wire.thp.channel_manager
trezor.wire.thp.checksum
import trezor.wire.thp.checksum
trezor.wire.thp.control_byte
import trezor.wire.thp.control_byte
trezor.wire.thp.cpace
import trezor.wire.thp.cpace
trezor.wire.thp.crypto
import trezor.wire.thp.crypto
trezor.wire.thp.interface_manager
import trezor.wire.thp.interface_manager
trezor.wire.thp.memory_manager
import trezor.wire.thp.memory_manager
trezor.wire.thp.pairing_context
import trezor.wire.thp.pairing_context
trezor.wire.thp.received_message_handler
import trezor.wire.thp.received_message_handler
trezor.wire.thp.session_context
import trezor.wire.thp.session_context
trezor.wire.thp.session_manager
import trezor.wire.thp.session_manager
trezor.wire.thp.thp_main
import trezor.wire.thp.thp_main
trezor.wire.thp.transmission_loop
import trezor.wire.thp.transmission_loop
trezor.wire.thp.writer
import trezor.wire.thp.writer
apps.thp apps.thp
import apps.thp import apps.thp
apps.thp.credential_manager apps.thp.credential_manager
import apps.thp.credential_manager import apps.thp.credential_manager
apps.thp.pairing
import apps.thp.pairing
if not utils.BITCOIN_ONLY: if not utils.BITCOIN_ONLY:
trezor.enums.BinanceOrderSide trezor.enums.BinanceOrderSide

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import storage.device as storage_device import storage.device as storage_device
from storage.cache_common import APP_COMMON_BUSY_DEADLINE_MS, APP_COMMON_SEED from storage.cache_common import APP_COMMON_BUSY_DEADLINE_MS, APP_COMMON_SEED
from trezor import TR, config, utils, wire, workflow from trezor import TR, config, utils, wire, workflow
from trezor.enums import HomescreenFormat, MessageType from trezor.enums import HomescreenFormat, MessageType, ThpMessageType
from trezor.messages import Success, UnlockPath from trezor.messages import Success, UnlockPath
from trezor.ui.layouts import confirm_action from trezor.ui.layouts import confirm_action
from trezor.wire import context from trezor.wire import context
@ -27,6 +27,9 @@ if TYPE_CHECKING:
) )
from trezor.wire import Handler, Msg from trezor.wire import Handler, Msg
if utils.USE_THP:
from trezor.messages import Failure, ThpCreateNewSession
_SCREENSAVER_IS_ON = False _SCREENSAVER_IS_ON = False
@ -204,33 +207,103 @@ def get_features() -> Features:
return f return f
async def handle_Initialize(msg: Initialize) -> Features: if utils.USE_THP:
import storage.cache_codec as cache_codec
session_id = cache_codec.start_session(msg.session_id) async def handle_ThpCreateNewSession(
message: ThpCreateNewSession,
) -> Success | Failure:
"""
Creates a new `ThpSession` based on the provided parameters and returns a
`Success` message on success.
if not utils.BITCOIN_ONLY: Returns an appropriate `Failure` message if session creation fails.
from storage.cache_common import APP_COMMON_DERIVE_CARDANO """
from trezor import log, loop
from trezor.enums import FailureType
from trezor.messages import Failure
from trezor.wire import NotInitialized
from trezor.wire.context import get_context
from trezor.wire.errors import ActionCancelled, DataError
from trezor.wire.thp.session_context import GenericSessionContext
from trezor.wire.thp.session_manager import get_new_session_context
derive_cardano = context.cache_get_bool(APP_COMMON_DERIVE_CARDANO) from apps.common.seed import derive_and_store_roots
have_seed = context.cache_is_set(APP_COMMON_SEED)
if (
have_seed
and msg.derive_cardano is not None
and msg.derive_cardano != bool(derive_cardano)
):
# seed is already derived, and host wants to change derive_cardano setting
# => create a new session
cache_codec.end_current_session()
session_id = cache_codec.start_session()
have_seed = False
if not have_seed: ctx = get_context()
context.cache_set_bool(APP_COMMON_DERIVE_CARDANO, bool(msg.derive_cardano))
features = get_features() # Assert that context `ctx` is `GenericSessionContext`
features.session_id = session_id assert isinstance(ctx, GenericSessionContext)
return features
channel = ctx.channel
session_id = ctx.session_id
# Do not use `ctx` beyond this point, as it is techically
# allowed to change in between await statements
if not 0 <= session_id <= 255:
return Failure(
code=FailureType.DataError,
message="Invalid session_id for session creation.",
)
new_session = get_new_session_context(
channel_ctx=channel, session_id=session_id
)
try:
await unlock_device()
await derive_and_store_roots(new_session, message)
except DataError as e:
return Failure(code=FailureType.DataError, message=e.message)
except ActionCancelled as e:
return Failure(code=FailureType.ActionCancelled, message=e.message)
except NotInitialized as e:
return Failure(code=FailureType.NotInitialized, message=e.message)
# TODO handle other errors (`Exception` when "Cardano icarus secret is already set!")
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"New session with sid %d and passphrase %s created.",
session_id,
message.passphrase if message.passphrase is not None else "",
)
channel.sessions[new_session.session_id] = new_session
loop.schedule(new_session.handle())
return Success(message="New session created.")
else:
async def handle_Initialize(msg: Initialize) -> Features:
import storage.cache_codec as cache_codec
session_id = cache_codec.start_session(msg.session_id)
if not utils.BITCOIN_ONLY:
from storage.cache_common import APP_COMMON_DERIVE_CARDANO
derive_cardano = context.cache_get_bool(APP_COMMON_DERIVE_CARDANO)
have_seed = context.cache_is_set(APP_COMMON_SEED)
if (
have_seed
and msg.derive_cardano is not None
and msg.derive_cardano != bool(derive_cardano)
):
# seed is already derived, and host wants to change derive_cardano setting
# => create a new session
cache_codec.end_current_session()
session_id = cache_codec.start_session()
have_seed = False
if not have_seed:
context.cache_set_bool(
APP_COMMON_DERIVE_CARDANO, bool(msg.derive_cardano)
)
features = get_features()
features.session_id = session_id
return features
async def handle_GetFeatures(msg: GetFeatures) -> Features: async def handle_GetFeatures(msg: GetFeatures) -> Features:
@ -464,8 +537,12 @@ def boot() -> None:
MT = MessageType # local_cache_global MT = MessageType # local_cache_global
# Register workflow handlers # Register workflow handlers
if utils.USE_THP:
TMT = ThpMessageType
workflow_handlers.register(TMT.ThpCreateNewSession, handle_ThpCreateNewSession)
else:
workflow_handlers.register(MT.Initialize, handle_Initialize)
for msg_type, handler in [ for msg_type, handler in [
(MT.Initialize, handle_Initialize),
(MT.GetFeatures, handle_GetFeatures), (MT.GetFeatures, handle_GetFeatures),
(MT.Cancel, handle_Cancel), (MT.Cancel, handle_Cancel),
(MT.LockDevice, handle_LockDevice), (MT.LockDevice, handle_LockDevice),

View File

@ -6,9 +6,8 @@ from storage.cache_common import (
APP_CARDANO_ICARUS_TREZOR_SECRET, APP_CARDANO_ICARUS_TREZOR_SECRET,
APP_COMMON_DERIVE_CARDANO, APP_COMMON_DERIVE_CARDANO,
) )
from trezor import wire from trezor import utils, wire
from trezor.crypto import cardano from trezor.crypto import cardano
from trezor.wire import context
from apps.common import mnemonic from apps.common import mnemonic
from apps.common.seed import get_seed from apps.common.seed import get_seed
@ -21,6 +20,7 @@ if TYPE_CHECKING:
from trezor import messages from trezor import messages
from trezor.crypto import bip32 from trezor.crypto import bip32
from trezor.enums import CardanoDerivationType from trezor.enums import CardanoDerivationType
from trezor.wire.protocol_common import Context
from apps.common.keychain import Handler, MsgOut from apps.common.keychain import Handler, MsgOut
from apps.common.paths import Bip32Path from apps.common.paths import Bip32Path
@ -116,9 +116,9 @@ def is_minting_path(path: Bip32Path) -> bool:
return path[: len(MINTING_ROOT)] == MINTING_ROOT return path[: len(MINTING_ROOT)] == MINTING_ROOT
def derive_and_store_secrets(passphrase: str) -> None: def derive_and_store_secrets(ctx: Context, passphrase: str) -> None:
assert device.is_initialized() assert device.is_initialized()
assert context.cache_get_bool(APP_COMMON_DERIVE_CARDANO) assert ctx.cache.get_bool(APP_COMMON_DERIVE_CARDANO)
if not mnemonic.is_bip39(): if not mnemonic.is_bip39():
# nothing to do for SLIP-39, where we can derive the root from the main seed # nothing to do for SLIP-39, where we can derive the root from the main seed
@ -138,14 +138,13 @@ def derive_and_store_secrets(passphrase: str) -> None:
else: else:
icarus_trezor_secret = icarus_secret icarus_trezor_secret = icarus_secret
context.cache_set(APP_CARDANO_ICARUS_SECRET, icarus_secret) ctx.cache.set(APP_CARDANO_ICARUS_SECRET, icarus_secret)
context.cache_set(APP_CARDANO_ICARUS_TREZOR_SECRET, icarus_trezor_secret) ctx.cache.set(APP_CARDANO_ICARUS_TREZOR_SECRET, icarus_trezor_secret)
async def _get_keychain_bip39(derivation_type: CardanoDerivationType) -> Keychain: async def _get_keychain_bip39(derivation_type: CardanoDerivationType) -> Keychain:
from trezor.enums import CardanoDerivationType from trezor.enums import CardanoDerivationType
from trezor.wire import context
from apps.common.seed import derive_and_store_roots
if not device.is_initialized(): if not device.is_initialized():
raise wire.NotInitialized("Device is not initialized") raise wire.NotInitialized("Device is not initialized")
@ -164,10 +163,13 @@ async def _get_keychain_bip39(derivation_type: CardanoDerivationType) -> Keychai
# _get_secret # _get_secret
secret = context.cache_get(cache_entry) secret = context.cache_get(cache_entry)
if secret is None: if not utils.USE_THP:
await derive_and_store_roots() if secret is None:
secret = context.cache_get(cache_entry) from apps.common.seed import derive_and_store_roots_legacy
assert secret is not None
await derive_and_store_roots_legacy()
secret = context.cache_get(cache_entry)
assert secret is not None
root = cardano.from_secret(secret) root = cardano.from_secret(secret)
return Keychain(root) return Keychain(root)

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from storage.cache_common import APP_RECOVERY_REPEATED_BACKUP_UNLOCKED from storage.cache_common import APP_RECOVERY_REPEATED_BACKUP_UNLOCKED
from trezor import wire from trezor import utils, wire
from trezor.enums import MessageType from trezor.enums import MessageType
from trezor.wire import context from trezor.wire import context
from trezor.wire.message_handler import filters, remove_filter from trezor.wire.message_handler import filters, remove_filter
@ -24,14 +24,23 @@ def deactivate_repeated_backup() -> None:
remove_filter(_repeated_backup_filter) remove_filter(_repeated_backup_filter)
_ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = ( if utils.USE_THP:
MessageType.Initialize, _ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = (
MessageType.GetFeatures, MessageType.GetFeatures,
MessageType.EndSession, MessageType.EndSession,
MessageType.BackupDevice, MessageType.BackupDevice,
MessageType.WipeDevice, MessageType.WipeDevice,
MessageType.Cancel, MessageType.Cancel,
) )
else:
_ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = (
MessageType.Initialize,
MessageType.GetFeatures,
MessageType.EndSession,
MessageType.BackupDevice,
MessageType.WipeDevice,
MessageType.Cancel,
)
def _repeated_backup_filter(msg_type: int, prev_handler: Handler[Msg]) -> Handler[Msg]: def _repeated_backup_filter(msg_type: int, prev_handler: Handler[Msg]) -> Handler[Msg]:

View File

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from trezor import utils
from trezor.crypto import bip32 from trezor.crypto import bip32
from trezor.wire import DataError from trezor.wire import DataError
@ -172,6 +173,9 @@ async def get_keychain(
) -> Keychain: ) -> Keychain:
from .seed import get_seed from .seed import get_seed
if not utils.USE_THP:
pass
# try to ask for passphrase here
seed = await get_seed() seed = await get_seed()
keychain = Keychain(seed, curve, schemas, slip21_namespaces) keychain = Keychain(seed, curve, schemas, slip21_namespaces)
return keychain return keychain

View File

@ -1,84 +1,122 @@
from micropython import const from micropython import const
from typing import TYPE_CHECKING
import storage.device as storage_device import storage.device as storage_device
from trezor import utils
from trezor.wire import DataError from trezor.wire import DataError
_MAX_PASSPHRASE_LEN = const(50) _MAX_PASSPHRASE_LEN = const(50)
if TYPE_CHECKING:
from trezor.messages import ThpCreateNewSession
def is_enabled() -> bool: def is_enabled() -> bool:
return storage_device.is_passphrase_enabled() return storage_device.is_passphrase_enabled()
async def get() -> str: async def get_passphrase(msg: ThpCreateNewSession) -> str:
from trezor import workflow
if not is_enabled(): if not is_enabled():
return "" return ""
if msg.on_device or storage_device.get_passphrase_always_on_device():
passphrase = await _get_on_device()
else: else:
workflow.close_others() # request exclusive UI access passphrase = msg.passphrase or ""
if storage_device.get_passphrase_always_on_device(): if passphrase:
from trezor.ui.layouts import request_passphrase_on_device await _handle_displaying_passphrase_from_host(passphrase)
passphrase = await request_passphrase_on_device(_MAX_PASSPHRASE_LEN) if len(passphrase.encode()) > _MAX_PASSPHRASE_LEN:
else: raise DataError(f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes")
passphrase = await _request_on_host()
if len(passphrase.encode()) > _MAX_PASSPHRASE_LEN:
raise DataError(f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes")
return passphrase
async def _request_on_host() -> str:
from trezor import TR
from trezor.messages import PassphraseAck, PassphraseRequest
from trezor.ui.layouts import request_passphrase_on_host
from trezor.wire.context import call
request_passphrase_on_host()
request = PassphraseRequest()
ack = await call(request, PassphraseAck)
passphrase = ack.passphrase # local_cache_attribute
if ack.on_device:
from trezor.ui.layouts import request_passphrase_on_device
if passphrase is not None:
raise DataError("Passphrase provided when it should not be")
return await request_passphrase_on_device(_MAX_PASSPHRASE_LEN)
if passphrase is None:
raise DataError(
"Passphrase not provided and on_device is False. Use empty string to set an empty passphrase."
)
# non-empty passphrase
if passphrase:
from trezor.ui.layouts import confirm_action, confirm_blob
# We want to hide the passphrase, or show it, according to settings.
if storage_device.get_hide_passphrase_from_host():
await confirm_action(
"passphrase_host1_hidden",
TR.passphrase__wallet,
description=TR.passphrase__from_host_not_shown,
prompt_screen=True,
prompt_title=TR.passphrase__access_wallet,
)
else:
await confirm_action(
"passphrase_host1",
TR.passphrase__wallet,
description=TR.passphrase__next_screen_will_show_passphrase,
verb=TR.buttons__continue,
)
await confirm_blob(
"passphrase_host2",
TR.passphrase__title_confirm,
passphrase,
info=False,
)
return passphrase return passphrase
async def _get_on_device() -> str:
from trezor import workflow
from trezor.ui.layouts import request_passphrase_on_device
workflow.close_others() # request exclusive UI access
passphrase = await request_passphrase_on_device(_MAX_PASSPHRASE_LEN)
return passphrase
async def _handle_displaying_passphrase_from_host(passphrase: str) -> None:
from trezor import TR
from trezor.ui.layouts import confirm_action, confirm_blob
# We want to hide the passphrase, or show it, according to settings.
if storage_device.get_hide_passphrase_from_host():
await confirm_action(
"passphrase_host1_hidden",
TR.passphrase__wallet,
description=TR.passphrase__from_host_not_shown,
prompt_screen=True,
prompt_title=TR.passphrase__access_wallet,
)
else:
await confirm_action(
"passphrase_host1",
TR.passphrase__wallet,
description=TR.passphrase__next_screen_will_show_passphrase,
verb=TR.buttons__continue,
)
await confirm_blob(
"passphrase_host2",
TR.passphrase__title_confirm,
passphrase,
)
if not utils.USE_THP:
async def get() -> str:
from trezor import workflow
if not is_enabled():
return ""
else:
workflow.close_others() # request exclusive UI access
if storage_device.get_passphrase_always_on_device():
from trezor.ui.layouts import request_passphrase_on_device
passphrase = await request_passphrase_on_device(_MAX_PASSPHRASE_LEN)
else:
passphrase = await _request_on_host()
if len(passphrase.encode()) > _MAX_PASSPHRASE_LEN:
raise DataError(
f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes"
)
return passphrase
async def _request_on_host() -> str:
from trezor.messages import PassphraseAck, PassphraseRequest
from trezor.ui.layouts import request_passphrase_on_host
from trezor.wire.context import call
request_passphrase_on_host()
request = PassphraseRequest()
ack = await call(request, PassphraseAck)
passphrase = ack.passphrase # local_cache_attribute
if ack.on_device:
from trezor.ui.layouts import request_passphrase_on_device
if passphrase is not None:
raise DataError("Passphrase provided when it should not be")
return await request_passphrase_on_device(_MAX_PASSPHRASE_LEN)
if passphrase is None:
raise DataError(
"Passphrase not provided and on_device is False. Use empty string to set an empty passphrase."
)
# non-empty passphrase
if passphrase:
await _handle_displaying_passphrase_from_host(passphrase)
return passphrase

View File

@ -5,14 +5,18 @@ from storage.cache_common import APP_COMMON_SEED, APP_COMMON_SEED_WITHOUT_PASSPH
from trezor import utils from trezor import utils
from trezor.crypto import hmac from trezor.crypto import hmac
from trezor.wire import context from trezor.wire import context
from trezor.wire.context import get_context
from trezor.wire.errors import DataError
from apps.common import cache from apps.common import cache
from . import mnemonic from . import mnemonic
from .passphrase import get as get_passphrase from .passphrase import get_passphrase as get_passphrase
if TYPE_CHECKING: if TYPE_CHECKING:
from trezor.crypto import bip32 from trezor.crypto import bip32
from trezor.messages import ThpCreateNewSession
from trezor.wire.protocol_common import Context
from .paths import Bip32Path, Slip21Path from .paths import Bip32Path, Slip21Path
@ -22,6 +26,9 @@ if not utils.BITCOIN_ONLY:
APP_COMMON_DERIVE_CARDANO, APP_COMMON_DERIVE_CARDANO,
) )
if not utils.USE_THP:
from .passphrase import get as get_passphrase_legacy
class Slip21Node: class Slip21Node:
""" """
@ -54,51 +61,111 @@ class Slip21Node:
return Slip21Node(data=self.data) return Slip21Node(data=self.data)
if not utils.BITCOIN_ONLY: if utils.USE_THP:
# === Cardano variant ===
# We want to derive both the normal seed and the Cardano seed together, AND
# expose a method for Cardano to do the same
async def derive_and_store_roots() -> None: async def get_seed() -> bytes: # type: ignore [Function declaration "get_seed" is obscured by a declaration of the same name]
from trezor import wire
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
need_seed = not context.cache_is_set(APP_COMMON_SEED)
need_cardano_secret = context.cache_get_bool(
APP_COMMON_DERIVE_CARDANO
) and not context.cache_is_set(APP_CARDANO_ICARUS_SECRET)
if not need_seed and not need_cardano_secret:
return
passphrase = await get_passphrase()
if need_seed:
common_seed = mnemonic.get_seed(passphrase)
context.cache_set(APP_COMMON_SEED, common_seed)
if need_cardano_secret:
from apps.cardano.seed import derive_and_store_secrets
derive_and_store_secrets(passphrase)
@cache.stored_async(APP_COMMON_SEED)
async def get_seed() -> bytes:
await derive_and_store_roots()
common_seed = context.cache_get(APP_COMMON_SEED) common_seed = context.cache_get(APP_COMMON_SEED)
assert common_seed is not None assert common_seed is not None
return common_seed return common_seed
else: if utils.BITCOIN_ONLY:
# === Bitcoin-only variant === # === Bitcoin_only variant ===
# We use the simple version of `get_seed` that never needs to derive anything else. # We want to derive the normal seed ONLY
@cache.stored_async(APP_COMMON_SEED) async def derive_and_store_roots(
async def get_seed() -> bytes: ctx: Context, msg: ThpCreateNewSession
passphrase = await get_passphrase() ) -> None:
return mnemonic.get_seed(passphrase)
if msg.passphrase is not None and msg.on_device:
raise DataError("Passphrase provided when it shouldn't be!")
if ctx.cache.is_set(APP_COMMON_SEED):
raise Exception("Seed is already set!")
from trezor import wire
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
passphrase = await get_passphrase(msg)
common_seed = mnemonic.get_seed(passphrase)
ctx.cache.set(APP_COMMON_SEED, common_seed)
else:
# === Cardano variant ===
# We want to derive both the normal seed and the Cardano seed together
async def derive_and_store_roots(
ctx: Context, msg: ThpCreateNewSession
) -> None:
if msg.passphrase is not None and msg.on_device:
raise DataError("Passphrase provided when it shouldn't be!")
from trezor import wire
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
if ctx.cache.is_set(APP_CARDANO_ICARUS_SECRET):
raise Exception("Cardano icarus secret is already set!")
passphrase = await get_passphrase(msg)
common_seed = mnemonic.get_seed(passphrase)
ctx.cache.set(APP_COMMON_SEED, common_seed)
if msg.derive_cardano:
from apps.cardano.seed import derive_and_store_secrets
ctx.cache.set_bool(APP_COMMON_DERIVE_CARDANO, True)
derive_and_store_secrets(ctx, passphrase)
else:
if utils.BITCOIN_ONLY:
# === Bitcoin-only variant ===
# We use the simple version of `get_seed` that never needs to derive anything else.
@cache.stored_async(APP_COMMON_SEED)
async def get_seed() -> bytes:
passphrase = await get_passphrase_legacy()
return mnemonic.get_seed(passphrase=passphrase)
else:
# === Cardano variant ===
# We want to derive both the normal seed and the Cardano seed together, AND
# expose a method for Cardano to do the same
@cache.stored_async(APP_COMMON_SEED)
async def get_seed() -> bytes:
await derive_and_store_roots_legacy()
common_seed = context.cache_get(APP_COMMON_SEED)
assert common_seed is not None
return common_seed
async def derive_and_store_roots_legacy() -> None:
from trezor import wire
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
ctx = get_context()
need_seed = not ctx.cache.is_set(APP_COMMON_SEED)
need_cardano_secret = ctx.cache.get_bool(
APP_COMMON_DERIVE_CARDANO
) and not ctx.cache.is_set(APP_CARDANO_ICARUS_SECRET)
if not need_seed and not need_cardano_secret:
return
passphrase = await get_passphrase_legacy()
if need_seed:
common_seed = mnemonic.get_seed(passphrase)
ctx.cache.set(APP_COMMON_SEED, common_seed)
if need_cardano_secret:
from apps.cardano.seed import derive_and_store_secrets
derive_and_store_secrets(ctx, passphrase)
@cache.stored(APP_COMMON_SEED_WITHOUT_PASSPHRASE) @cache.stored(APP_COMMON_SEED_WITHOUT_PASSPHRASE)

View File

@ -1,3 +1,5 @@
from trezor.wire import message_handler
if not __debug__: if not __debug__:
from trezor.utils import halt from trezor.utils import halt
@ -22,20 +24,23 @@ if __debug__:
from trezor.messages import ( from trezor.messages import (
DebugLinkDecision, DebugLinkDecision,
DebugLinkEraseSdCard, DebugLinkEraseSdCard,
DebugLinkGetPairingInfo,
DebugLinkGetState, DebugLinkGetState,
DebugLinkOptigaSetSecMax, DebugLinkOptigaSetSecMax,
DebugLinkPairingInfo,
DebugLinkRecordScreen, DebugLinkRecordScreen,
DebugLinkReseedRandom, DebugLinkReseedRandom,
DebugLinkState, DebugLinkState,
) )
from trezor.ui import Layout from trezor.ui import Layout
from trezor.wire import WireInterface, context from trezor.wire import WireInterface
from trezor.wire.protocol_common import Context
Handler = Callable[[Any], Awaitable[Any]] Handler = Callable[[Any], Awaitable[Any]]
layout_change_box = loop.mailbox() layout_change_box = loop.mailbox()
DEBUG_CONTEXT: context.Context | None = None DEBUG_CONTEXT: Context | None = None
REFRESH_INDEX = 0 REFRESH_INDEX = 0
@ -70,9 +75,7 @@ if __debug__:
"layout deadlock detected (did you send a ButtonAck?)" "layout deadlock detected (did you send a ButtonAck?)"
) )
async def return_layout_change( async def return_layout_change(ctx: Context, detect_deadlock: bool = False) -> None:
ctx: wire.protocol_common.Context, detect_deadlock: bool = False
) -> None:
# set up the wait # set up the wait
storage.layout_watcher = True storage.layout_watcher = True
@ -265,6 +268,42 @@ if __debug__:
tokens=tokens, tokens=tokens,
) )
async def dispatch_DebugLinkGetPairingInfo(
msg: DebugLinkGetPairingInfo,
) -> DebugLinkPairingInfo | None:
if not utils.USE_THP:
raise RuntimeError("Trezor does not support THP")
if msg.channel_id is None:
raise RuntimeError("Invalid DebugLinkGetPairingInfo message")
from trezor.wire.thp.channel import Channel
from trezor.wire.thp.pairing_context import PairingContext
from trezor.wire.thp.thp_main import _CHANNELS
channel_id = int.from_bytes(msg.channel_id, "big")
channel: Channel | None = None
ctx: PairingContext | None = None
try:
channel = _CHANNELS[channel_id]
ctx = channel.connection_context
except KeyError:
pass
if ctx is None or not isinstance(ctx, PairingContext):
raise RuntimeError("Trezor is not in pairing mode")
ctx.nfc_secret_host = msg.nfc_secret_host
ctx.handshake_hash_host = msg.handshake_hash
from trezor.messages import DebugLinkPairingInfo
return DebugLinkPairingInfo(
channel_id=ctx.channel_id,
handshake_hash=ctx.channel_ctx.get_handshake_hash(),
code_entry_code=ctx.code_code_entry,
code_qr_code=ctx.code_qr_code,
nfc_secret_trezor=ctx.nfc_secret,
)
async def dispatch_DebugLinkGetState( async def dispatch_DebugLinkGetState(
msg: DebugLinkGetState, msg: DebugLinkGetState,
) -> DebugLinkState | None: ) -> DebugLinkState | None:
@ -395,7 +434,6 @@ if __debug__:
ctx.iface.iface_num(), ctx.iface.iface_num(),
msg_type, msg_type,
) )
if msg.type not in WORKFLOW_HANDLERS: if msg.type not in WORKFLOW_HANDLERS:
await ctx.write(wire.message_handler.unexpected_message()) await ctx.write(wire.message_handler.unexpected_message())
continue continue
@ -408,7 +446,7 @@ if __debug__:
await ctx.write(Success()) await ctx.write(Success())
continue continue
req_msg = wire.message_handler.wrap_protobuf_load(msg.data, req_type) req_msg = message_handler.wrap_protobuf_load(msg.data, req_type)
try: try:
res_msg = await WORKFLOW_HANDLERS[msg.type](req_msg) res_msg = await WORKFLOW_HANDLERS[msg.type](req_msg)
except Exception as exc: except Exception as exc:
@ -427,6 +465,7 @@ if __debug__:
WORKFLOW_HANDLERS: dict[int, Handler] = { WORKFLOW_HANDLERS: dict[int, Handler] = {
MessageType.DebugLinkDecision: dispatch_DebugLinkDecision, MessageType.DebugLinkDecision: dispatch_DebugLinkDecision,
MessageType.DebugLinkGetState: dispatch_DebugLinkGetState, MessageType.DebugLinkGetState: dispatch_DebugLinkGetState,
MessageType.DebugLinkGetPairingInfo: dispatch_DebugLinkGetPairingInfo,
MessageType.DebugLinkReseedRandom: dispatch_DebugLinkReseedRandom, MessageType.DebugLinkReseedRandom: dispatch_DebugLinkReseedRandom,
MessageType.DebugLinkRecordScreen: dispatch_DebugLinkRecordScreen, MessageType.DebugLinkRecordScreen: dispatch_DebugLinkRecordScreen,
MessageType.DebugLinkEraseSdCard: dispatch_DebugLinkEraseSdCard, MessageType.DebugLinkEraseSdCard: dispatch_DebugLinkEraseSdCard,

View File

@ -89,7 +89,7 @@ async def reboot_to_bootloader(msg: RebootToBootloader) -> NoReturn:
boot_args = None boot_args = None
ctx = get_context() ctx = get_context()
await ctx.write(Success(message="Rebooting")) await ctx.write_force(Success(message="Rebooting"))
# make sure the outgoing USB buffer is flushed # make sure the outgoing USB buffer is flushed
await loop.wait(ctx.iface.iface_num() | io.POLL_WRITE) await loop.wait(ctx.iface.iface_num() | io.POLL_WRITE)
# reboot to the bootloader, pass the firmware header hash if any # reboot to the bootloader, pass the firmware header hash if any

View File

@ -24,6 +24,7 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
from trezor import TR, config, wire, workflow from trezor import TR, config, wire, workflow
from trezor.enums import BackupType, ButtonRequestType from trezor.enums import BackupType, ButtonRequestType
from trezor.ui.layouts import confirm_action, confirm_reset_device from trezor.ui.layouts import confirm_action, confirm_reset_device
from trezor.wire.context import try_get_ctx_ids
from apps.common import mnemonic from apps.common import mnemonic
from apps.common.request_pin import ( from apps.common.request_pin import (
@ -69,8 +70,8 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
if recovery_type == RecoveryType.NormalRecovery: if recovery_type == RecoveryType.NormalRecovery:
await confirm_reset_device(recovery=True) await confirm_reset_device(recovery=True)
# wipe storage to make sure the device is in a clear state # wipe storage to make sure the device is in a clear state (except protocol cache)
storage.reset() storage.reset(excluded=try_get_ctx_ids())
# set up pin if requested # set up pin if requested
if msg.pin_protection: if msg.pin_protection:

View File

@ -3,8 +3,9 @@ from typing import TYPE_CHECKING
import storage.device as storage_device import storage.device as storage_device
import storage.recovery as storage_recovery import storage.recovery as storage_recovery
import storage.recovery_shares as storage_recovery_shares import storage.recovery_shares as storage_recovery_shares
from trezor import TR, wire from trezor import TR, utils, wire
from trezor.messages import Success from trezor.messages import Success
from trezor.wire import message_handler
from apps.common import backup_types from apps.common import backup_types
@ -38,18 +39,27 @@ async def recovery_process() -> Success:
recovery_type = storage_recovery.get_type() recovery_type = storage_recovery.get_type()
wire.message_handler.AVOID_RESTARTING_FOR = ( if utils.USE_THP:
MessageType.Initialize, message_handler.AVOID_RESTARTING_FOR = (
MessageType.GetFeatures, MessageType.GetFeatures,
MessageType.EndSession, MessageType.EndSession,
) )
else:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.Initialize,
MessageType.GetFeatures,
MessageType.EndSession,
)
try: try:
return await _continue_recovery_process() return await _continue_recovery_process()
except recover.RecoveryAborted: except recover.RecoveryAborted:
storage_recovery.end_progress() storage_recovery.end_progress()
backup.deactivate_repeated_backup() backup.deactivate_repeated_backup()
if recovery_type == RecoveryType.NormalRecovery: if recovery_type == RecoveryType.NormalRecovery:
storage.wipe() from trezor.wire.context import try_get_ctx_ids
storage.wipe(clear_cache=False)
storage.wipe_cache(excluded=try_get_ctx_ids())
raise wire.ActionCancelled raise wire.ActionCancelled
@ -59,11 +69,17 @@ async def _continue_repeated_backup() -> None:
from apps.common import backup from apps.common import backup
from apps.management.backup_device import perform_backup from apps.management.backup_device import perform_backup
wire.message_handler.AVOID_RESTARTING_FOR = ( if utils.USE_THP:
MessageType.Initialize, message_handler.AVOID_RESTARTING_FOR = (
MessageType.GetFeatures, MessageType.GetFeatures,
MessageType.EndSession, MessageType.EndSession,
) )
else:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.Initialize,
MessageType.GetFeatures,
MessageType.EndSession,
)
try: try:
await perform_backup(is_repeated_backup=True) await perform_backup(is_repeated_backup=True)

View File

@ -38,7 +38,7 @@ async def reset_device(msg: ResetDevice) -> Success:
prompt_backup, prompt_backup,
show_wallet_created_success, show_wallet_created_success,
) )
from trezor.wire.context import call from trezor.wire.context import call, try_get_ctx_ids
from apps.common.request_pin import request_pin_confirm from apps.common.request_pin import request_pin_confirm
@ -60,8 +60,8 @@ async def reset_device(msg: ResetDevice) -> Success:
# Rendering empty loader so users do not feel a freezing screen # Rendering empty loader so users do not feel a freezing screen
render_empty_loader(config.StorageMessage.PROCESSING_MSG) render_empty_loader(config.StorageMessage.PROCESSING_MSG)
# wipe storage to make sure the device is in a clear state # wipe storage to make sure the device is in a clear state (except protocol cache)
storage.reset() storage.reset(excluded=try_get_ctx_ids())
# Check backup type, perform type-specific handling # Check backup type, perform type-specific handling
if backup_types.is_slip39_backup_type(backup_type): if backup_types.is_slip39_backup_type(backup_type):
@ -139,7 +139,7 @@ async def reset_device(msg: ResetDevice) -> Success:
if perform_backup: if perform_backup:
await layout.show_backup_success() await layout.show_backup_success()
return Success(message="Initialized") return Success(message="Initialized") # TODO: Why "Initialized?"
async def _entropy_check(secret: bytes) -> bool: async def _entropy_check(secret: bytes) -> bool:

View File

@ -1,12 +1,19 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from trezor.wire.context import get_context, try_get_ctx_ids
if TYPE_CHECKING: if TYPE_CHECKING:
from trezor.messages import Success, WipeDevice from typing import NoReturn
from trezor.messages import WipeDevice
if __debug__:
from trezor import log
async def wipe_device(msg: WipeDevice) -> Success: async def wipe_device(msg: WipeDevice) -> NoReturn:
import storage import storage
from trezor import TR, config, translations from trezor import TR, config, loop, translations
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.messages import Success from trezor.messages import Success
from trezor.pin import render_empty_loader from trezor.pin import render_empty_loader
@ -26,16 +33,31 @@ async def wipe_device(msg: WipeDevice) -> Success:
br_code=ButtonRequestType.WipeDevice, br_code=ButtonRequestType.WipeDevice,
) )
if __debug__:
log.debug(__name__, "Device wipe - start")
# start an empty progress screen so that the screen is not blank while waiting # start an empty progress screen so that the screen is not blank while waiting
render_empty_loader(config.StorageMessage.PROCESSING_MSG) render_empty_loader(config.StorageMessage.PROCESSING_MSG)
# wipe storage # wipe storage
storage.wipe() storage.wipe(clear_cache=False)
# clear cache - exclude current context
storage.wipe_cache(excluded=try_get_ctx_ids())
# erase translations # erase translations
translations.deinit() translations.deinit()
translations.erase() translations.erase()
try:
await get_context().write_force(Success(message="Device wiped"))
except Exception:
if __debug__:
log.debug(__name__, "Failed to send Success message after wipe.")
pass
storage.wipe_cache()
# reload settings # reload settings
reload_settings_from_storage() reload_settings_from_storage()
loop.clear()
return Success(message="Device wiped") if __debug__:
log.debug(__name__, "Device wipe - finished")

View File

@ -63,17 +63,26 @@ def issue_credential(
return credential_raw return credential_raw
def validate_credential( def decode_credential(
encoded_pairing_credential_message: bytes, encoded_pairing_credential_message: bytes,
) -> ThpPairingCredential:
"""
Decode a protobuf encoded pairing credential.
"""
expected_type = protobuf.type_for_name("ThpPairingCredential")
credential = wrap_protobuf_load(encoded_pairing_credential_message, expected_type)
assert ThpPairingCredential.is_type_of(credential)
return credential
def validate_credential(
credential: ThpPairingCredential,
host_static_pubkey: bytes, host_static_pubkey: bytes,
) -> bool: ) -> bool:
""" """
Validate a pairing credential binded to the provided host static public key. Validate a pairing credential binded to the provided host static public key.
""" """
cred_auth_key = derive_cred_auth_key() cred_auth_key = derive_cred_auth_key()
expected_type = protobuf.type_for_name("ThpPairingCredential")
credential = wrap_protobuf_load(encoded_pairing_credential_message, expected_type)
assert ThpPairingCredential.is_type_of(credential)
proto_msg = ThpAuthenticatedCredentialData( proto_msg = ThpAuthenticatedCredentialData(
host_static_pubkey=host_static_pubkey, host_static_pubkey=host_static_pubkey,
cred_metadata=credential.cred_metadata, cred_metadata=credential.cred_metadata,
@ -83,6 +92,27 @@ def validate_credential(
return mac == credential.mac return mac == credential.mac
def decode_and_validate_credential(
encoded_pairing_credential_message: bytes,
host_static_pubkey: bytes,
) -> bool:
"""
Decode a protobuf encoded pairing credential and validate it
binded to the provided host static public key.
"""
credential = decode_credential(encoded_pairing_credential_message)
return validate_credential(credential, host_static_pubkey)
def is_credential_autoconnect(credential: ThpPairingCredential) -> bool:
assert ThpPairingCredential.is_type_of(credential)
if credential.cred_metadata is None:
return False
if credential.cred_metadata.autoconnect is None:
return False
return credential.cred_metadata.autoconnect
def _encode_message_into_new_buffer(msg: protobuf.MessageType) -> bytes: def _encode_message_into_new_buffer(msg: protobuf.MessageType) -> bytes:
msg_len = protobuf.encoded_length(msg) msg_len = protobuf.encoded_length(msg)
new_buffer = bytearray(msg_len) new_buffer = bytearray(msg_len)

View File

@ -0,0 +1,469 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import protobuf
from trezor.crypto import random
from trezor.crypto.hashlib import sha256
from trezor.enums import ThpMessageType, ThpPairingMethod
from trezor.messages import (
Cancel,
ThpCodeEntryChallenge,
ThpCodeEntryCommitment,
ThpCodeEntryCpaceHostTag,
ThpCodeEntryCpaceTrezor,
ThpCodeEntrySecret,
ThpCredentialMetadata,
ThpCredentialRequest,
ThpCredentialResponse,
ThpEndRequest,
ThpEndResponse,
ThpNfcTagHost,
ThpNfcTagTrezor,
ThpPairingPreparationsFinished,
ThpPairingRequest,
ThpQrCodeSecret,
ThpQrCodeTag,
ThpSelectMethod,
)
from trezor.wire import message_handler
from trezor.wire.context import UnexpectedMessageException
from trezor.wire.errors import SilentError, UnexpectedMessage
from trezor.wire.thp import ChannelState, ThpError, crypto, get_enabled_pairing_methods
from trezor.wire.thp.pairing_context import PairingContext
from .credential_manager import is_credential_autoconnect, issue_credential
if __debug__:
from trezor import log
if TYPE_CHECKING:
from typing import Any, Callable, Concatenate, ParamSpec, Tuple
P = ParamSpec("P")
FuncWithContext = Callable[Concatenate[PairingContext, P], Any]
#
# Helpers - decorators
def check_state_and_log(
*allowed_states: ChannelState,
) -> Callable[[FuncWithContext], FuncWithContext]:
def decorator(f: FuncWithContext) -> FuncWithContext:
def inner(context: PairingContext, *args: P.args, **kwargs: P.kwargs) -> object:
_check_state(context, *allowed_states)
if __debug__:
try:
log.debug(__name__, "started %s", f.__name__)
except AttributeError:
log.debug(
__name__,
"started a function that cannot be named, because it raises AttributeError, eg. closure",
)
return f(context, *args, **kwargs)
return inner
return decorator
def check_method_is_allowed_and_selected(
pairing_method: ThpPairingMethod,
) -> Callable[[FuncWithContext], FuncWithContext]:
def decorator(f: FuncWithContext) -> FuncWithContext:
def inner(context: PairingContext, *args: P.args, **kwargs: P.kwargs) -> object:
_check_method_is_allowed(context, pairing_method)
_check_method_is_selected(context, pairing_method)
return f(context, *args, **kwargs)
return inner
return decorator
#
# Pairing handlers
@check_state_and_log(ChannelState.TP0)
async def handle_pairing_request(
ctx: PairingContext, message: protobuf.MessageType
) -> ThpEndResponse:
if not ThpPairingRequest.is_type_of(message):
raise UnexpectedMessage("Unexpected message")
ctx.host_name = message.host_name or ""
await ctx.show_pairing_dialogue()
assert ThpSelectMethod.MESSAGE_WIRE_TYPE is not None
select_method_msg = await ctx.read(
[
ThpSelectMethod.MESSAGE_WIRE_TYPE,
]
)
assert ThpSelectMethod.is_type_of(select_method_msg)
assert select_method_msg.selected_pairing_method is not None
ctx.set_selected_method(select_method_msg.selected_pairing_method)
if ctx.selected_method == ThpPairingMethod.SkipPairing:
return await _end_pairing(ctx)
while True:
await _prepare_pairing(ctx)
ctx.channel_ctx.set_channel_state(ChannelState.TP3)
try:
# Should raise UnexpectedMessageException
await ctx.show_pairing_method_screen()
except UnexpectedMessageException as e:
raw_response = e.msg
name = message_handler.get_msg_name(raw_response.type)
if name is None:
req_type = protobuf.type_for_wire(raw_response.type)
else:
req_type = protobuf.type_for_name(name)
response = message_handler.wrap_protobuf_load(raw_response.data, req_type)
if Cancel.is_type_of(response):
ctx.channel_ctx.clear()
raise SilentError("Action was cancelled by the Host")
if ThpSelectMethod.is_type_of(response):
assert response.selected_pairing_method is not None
ctx.set_selected_method(response.selected_pairing_method)
ctx.channel_ctx.set_channel_state(ChannelState.TP1)
else:
break
response: protobuf.MessageType = await _handle_different_pairing_methods(
ctx, response
)
return await handle_credential_phase(
ctx,
message=response,
show_connection_dialog=False,
)
@check_state_and_log(ChannelState.TC1)
async def handle_credential_phase(
ctx: PairingContext,
message: protobuf.MessageType,
show_connection_dialog: bool = True,
) -> ThpEndResponse:
autoconnect: bool = False
credential = ctx.channel_ctx.credential
if credential is not None:
autoconnect = is_credential_autoconnect(credential)
if credential.cred_metadata is not None:
ctx.host_name = credential.cred_metadata.host_name
if ctx.host_name is None:
raise Exception("Credential does not have a hostname")
if show_connection_dialog and not autoconnect:
await ctx.show_connection_dialogue()
while ThpCredentialRequest.is_type_of(message):
message = await _handle_credential_request(ctx, message)
return await _handle_end_request(ctx, message)
async def _prepare_pairing(ctx: PairingContext) -> None:
ctx.channel_ctx.set_channel_state(ChannelState.TP1)
if ctx.selected_method == ThpPairingMethod.CodeEntry:
await _handle_code_entry_is_selected(ctx)
elif ctx.selected_method == ThpPairingMethod.NFC:
await _handle_nfc_is_selected(ctx)
elif ctx.selected_method == ThpPairingMethod.QrCode:
await _handle_qr_code_is_selected(ctx)
else:
raise Exception() # TODO unknown pairing method
@check_state_and_log(ChannelState.TP1)
async def _handle_code_entry_is_selected(ctx: PairingContext) -> None:
if ctx.code_entry_secret is None:
await _handle_code_entry_is_selected_first_time(ctx)
else:
await ctx.write_force(ThpPairingPreparationsFinished())
async def _handle_code_entry_is_selected_first_time(ctx: PairingContext) -> None:
from trezor.wire.thp.cpace import Cpace
ctx.code_entry_secret = random.bytes(16)
commitment = sha256(ctx.code_entry_secret).digest()
challenge_message = await ctx.call(
ThpCodeEntryCommitment(commitment=commitment), ThpCodeEntryChallenge
)
ctx.channel_ctx.set_channel_state(ChannelState.TP2)
if not ThpCodeEntryChallenge.is_type_of(challenge_message):
raise UnexpectedMessage("Unexpected message")
if challenge_message.challenge is None:
raise Exception("Invalid message")
sha_ctx = sha256(ThpPairingMethod.CodeEntry.to_bytes(1, "big"))
sha_ctx.update(ctx.channel_ctx.get_handshake_hash())
sha_ctx.update(ctx.code_entry_secret)
sha_ctx.update(challenge_message.challenge)
code_code_entry_hash = sha_ctx.digest()
ctx.code_code_entry = int.from_bytes(code_code_entry_hash, "big") % 1000000
ctx.cpace = Cpace(
ctx.channel_ctx.get_handshake_hash(),
)
assert ctx.code_code_entry is not None
ctx.cpace.generate_keys_and_secret(ctx.code_code_entry.to_bytes(6, "big"))
await ctx.write_force(
ThpCodeEntryCpaceTrezor(cpace_trezor_public_key=ctx.cpace.trezor_public_key)
)
@check_state_and_log(ChannelState.TP1)
async def _handle_nfc_is_selected(ctx: PairingContext) -> None:
ctx.nfc_secret = random.bytes(16)
await ctx.write_force(ThpPairingPreparationsFinished())
@check_state_and_log(ChannelState.TP1)
async def _handle_qr_code_is_selected(ctx: PairingContext) -> None:
ctx.qr_code_secret = random.bytes(16)
sha_ctx = sha256(ThpPairingMethod.QrCode.to_bytes(1, "big"))
sha_ctx.update(ctx.channel_ctx.get_handshake_hash())
sha_ctx.update(ctx.qr_code_secret)
ctx.code_qr_code = sha_ctx.digest()[:16]
await ctx.write_force(ThpPairingPreparationsFinished())
@check_state_and_log(ChannelState.TP3)
async def _handle_different_pairing_methods(
ctx: PairingContext, response: protobuf.MessageType
) -> protobuf.MessageType:
if ThpCodeEntryCpaceHostTag.is_type_of(response):
return await _handle_code_entry_cpace(ctx, response)
if ThpQrCodeTag.is_type_of(response):
return await _handle_qr_code_tag(ctx, response)
if ThpNfcTagHost.is_type_of(response):
return await _handle_nfc_tag(ctx, response)
raise UnexpectedMessage("Unexpected message" + str(response))
@check_state_and_log(ChannelState.TP3)
@check_method_is_allowed_and_selected(ThpPairingMethod.CodeEntry)
async def _handle_code_entry_cpace(
ctx: PairingContext, message: protobuf.MessageType
) -> protobuf.MessageType:
if TYPE_CHECKING:
assert ThpCodeEntryCpaceHostTag.is_type_of(message)
if message.cpace_host_public_key is None:
raise ThpError(
"Message ThpCodeEntryCpaceHostTag is missing cpace_host_public_key"
)
if message.tag is None:
raise ThpError("Message ThpCodeEntryCpaceHostTag is missing tag")
ctx.cpace.compute_shared_secret(message.cpace_host_public_key)
expected_tag = sha256(ctx.cpace.shared_secret).digest()
if expected_tag != message.tag:
print(
"expected code entry tag:", hexlify(expected_tag).decode()
) # TODO remove after testing
print(
"expected code entry shared secret:",
hexlify(ctx.cpace.shared_secret).decode(),
) # TODO remove after testing
raise ThpError("Unexpected Code Entry Tag")
return await _handle_secret_reveal(
ctx,
msg=ThpCodeEntrySecret(secret=ctx.code_entry_secret),
)
@check_state_and_log(ChannelState.TP3)
@check_method_is_allowed_and_selected(ThpPairingMethod.QrCode)
async def _handle_qr_code_tag(
ctx: PairingContext, message: protobuf.MessageType
) -> protobuf.MessageType:
if TYPE_CHECKING:
assert isinstance(message, ThpQrCodeTag)
assert ctx.code_qr_code is not None
sha_ctx = sha256(ctx.channel_ctx.get_handshake_hash())
sha_ctx.update(ctx.code_qr_code)
expected_tag = sha_ctx.digest()
if expected_tag != message.tag:
print(
"expected qr code tag:", hexlify(expected_tag).decode()
) # TODO remove after testing
print(
"expected handshake hash:",
hexlify(ctx.channel_ctx.get_handshake_hash()).decode(),
) # TODO remove after testing
print(
"expected code qr code:",
hexlify(ctx.code_qr_code).decode(),
) # TODO remove after testing
print(
"expected secret:", hexlify(ctx.qr_code_secret or b"").decode()
) # TODO remove after testing
raise ThpError("Unexpected QR Code Tag")
return await _handle_secret_reveal(
ctx,
msg=ThpQrCodeSecret(secret=ctx.qr_code_secret),
)
@check_state_and_log(ChannelState.TP3)
@check_method_is_allowed_and_selected(ThpPairingMethod.NFC)
async def _handle_nfc_tag(
ctx: PairingContext, message: protobuf.MessageType
) -> protobuf.MessageType:
if TYPE_CHECKING:
assert isinstance(message, ThpNfcTagHost)
assert ctx.nfc_secret is not None
assert ctx.handshake_hash_host is not None
assert ctx.nfc_secret_host is not None
assert len(ctx.nfc_secret_host) == 16
sha_ctx = sha256(ThpPairingMethod.NFC.to_bytes(1, "big"))
sha_ctx.update(ctx.channel_ctx.get_handshake_hash())
sha_ctx.update(ctx.nfc_secret)
expected_tag = sha_ctx.digest()
if expected_tag != message.tag:
print(
"expected nfc tag:", hexlify(expected_tag).decode()
) # TODO remove after testing
raise ThpError("Unexpected NFC Unidirectional Tag")
if ctx.handshake_hash_host[:16] != ctx.channel_ctx.get_handshake_hash()[:16]:
raise ThpError("Handshake hash mismatch")
sha_ctx = sha256(ThpPairingMethod.NFC.to_bytes(1, "big"))
sha_ctx.update(ctx.channel_ctx.get_handshake_hash())
sha_ctx.update(ctx.nfc_secret_host)
trezor_tag = sha_ctx.digest()
return await _handle_secret_reveal(
ctx,
msg=ThpNfcTagTrezor(tag=trezor_tag),
)
@check_state_and_log(ChannelState.TP3, ChannelState.TP4)
async def _handle_secret_reveal(
ctx: PairingContext,
msg: protobuf.MessageType,
) -> protobuf.MessageType:
ctx.channel_ctx.set_channel_state(ChannelState.TC1)
return await ctx.call_any(
msg,
ThpMessageType.ThpCredentialRequest,
ThpMessageType.ThpEndRequest,
)
@check_state_and_log(ChannelState.TC1)
async def _handle_credential_request(
ctx: PairingContext, message: protobuf.MessageType
) -> protobuf.MessageType:
if not ThpCredentialRequest.is_type_of(message):
raise UnexpectedMessage("Unexpected message")
if message.host_static_pubkey is None:
raise Exception("Invalid message") # TODO change failure type
autoconnect: bool = False
if message.autoconnect is not None:
autoconnect = message.autoconnect
trezor_static_pubkey = crypto.get_trezor_static_pubkey()
credential_metadata = ThpCredentialMetadata(
host_name=ctx.host_name,
autoconnect=autoconnect,
)
credential = issue_credential(message.host_static_pubkey, credential_metadata)
return await ctx.call_any(
ThpCredentialResponse(
trezor_static_pubkey=trezor_static_pubkey, credential=credential
),
ThpMessageType.ThpCredentialRequest,
ThpMessageType.ThpEndRequest,
)
@check_state_and_log(ChannelState.TC1)
async def _handle_end_request(
ctx: PairingContext, message: protobuf.MessageType
) -> ThpEndResponse:
if not ThpEndRequest.is_type_of(message):
raise UnexpectedMessage("Unexpected message")
return await _end_pairing(ctx)
async def _end_pairing(ctx: PairingContext) -> ThpEndResponse:
ctx.channel_ctx.set_channel_state(ChannelState.ENCRYPTED_TRANSPORT)
return ThpEndResponse()
#
# Helpers - checkers
def _check_state(ctx: PairingContext, *allowed_states: ChannelState) -> None:
if ctx.channel_ctx.get_channel_state() not in allowed_states:
raise UnexpectedMessage("Unexpected message")
def _check_method_is_allowed(ctx: PairingContext, method: ThpPairingMethod) -> None:
if method not in get_enabled_pairing_methods(ctx.iface):
raise ThpError("Unexpected pairing method")
def _check_method_is_selected(ctx: PairingContext, method: ThpPairingMethod) -> None:
if method is not ctx.selected_method:
raise ThpError("Not selected pairing method")
#
# Helpers - getters
def _get_accepted_messages(ctx: PairingContext) -> Tuple[int, ...]:
r = _get_possible_pairing_methods(ctx)
mtype = Cancel.MESSAGE_WIRE_TYPE
r += (mtype,) if mtype is not None else ()
mtype = ThpSelectMethod.MESSAGE_WIRE_TYPE
r += (mtype,) if mtype is not None else ()
return r
def _get_possible_pairing_methods(ctx: PairingContext) -> Tuple[int, ...]:
r = tuple(
[
_get_message_type_for_method(ctx.selected_method),
]
)
return r
def _get_message_type_for_method(method: int) -> int:
if method is ThpPairingMethod.CodeEntry:
return ThpMessageType.ThpCodeEntryCpaceHostTag
if method is ThpPairingMethod.NFC:
return ThpMessageType.ThpNfcTagHost
if method is ThpPairingMethod.QrCode:
return ThpMessageType.ThpQrCodeTag
raise ValueError("Unexpected pairing method - no message type available")

View File

@ -1,12 +1,29 @@
# make sure to import cache unconditionally at top level so that it is imported (and retained) together with the storage module # make sure to import cache unconditionally at top level so that it is imported (and retained) together with the storage module
from typing import TYPE_CHECKING
from storage import cache, common, device from storage import cache, common, device
if TYPE_CHECKING:
from typing import Tuple
def wipe() -> None: pass
def wipe(clear_cache: bool = True) -> None:
"""
Wipes the storage.
If the device should communicate after wipe, use `clear_cache=False` and clear cache manually later using
`wipe_cache()`.
"""
from trezor import config from trezor import config
config.wipe() config.wipe()
cache.clear_all() if clear_cache:
cache.clear_all()
def wipe_cache(excluded: Tuple[bytes, bytes] | None = None) -> None:
cache.clear_all(excluded)
def init_unlocked() -> None: def init_unlocked() -> None:
@ -21,12 +38,13 @@ def init_unlocked() -> None:
common.set_bool(common.APP_DEVICE, device.INITIALIZED, True, public=True) common.set_bool(common.APP_DEVICE, device.INITIALIZED, True, public=True)
def reset() -> None: def reset(excluded: Tuple[bytes, bytes] | None) -> None:
""" """
Wipes storage but keeps the device id unchanged. Wipes storage but keeps the device id unchanged.
""" """
device_id = device.get_device_id() device_id = device.get_device_id()
wipe() wipe(clear_cache=False)
wipe_cache(excluded)
common.set(common.APP_DEVICE, device.DEVICE_ID, device_id.encode(), public=True) common.set(common.APP_DEVICE, device.DEVICE_ID, device_id.encode(), public=True)

View File

@ -1,26 +1,47 @@
import builtins import builtins
import gc import gc
from typing import TYPE_CHECKING
from storage import cache_codec
from storage.cache_common import SESSIONLESS_FLAG, SessionlessCache from storage.cache_common import SESSIONLESS_FLAG, SessionlessCache
from trezor import utils
if TYPE_CHECKING:
from typing import Tuple
pass
# Cache initialization # Cache initialization
_SESSIONLESS_CACHE = SessionlessCache() _SESSIONLESS_CACHE = SessionlessCache()
_PROTOCOL_CACHE = cache_codec
if utils.USE_THP:
from storage import cache_thp
_PROTOCOL_CACHE = cache_thp
else:
from storage import cache_codec
_PROTOCOL_CACHE = cache_codec
_PROTOCOL_CACHE.initialize() _PROTOCOL_CACHE.initialize()
_SESSIONLESS_CACHE.clear() _SESSIONLESS_CACHE.clear()
gc.collect() gc.collect()
def clear_all() -> None: def clear_all(excluded: Tuple[bytes, bytes] | None = None) -> None:
""" """
Clears all data from both the protocol cache and the sessionless cache. Clears all data from both the protocol cache and the sessionless cache.
""" """
global autolock_last_touch global autolock_last_touch
autolock_last_touch = None autolock_last_touch = None
_SESSIONLESS_CACHE.clear() _SESSIONLESS_CACHE.clear()
_PROTOCOL_CACHE.clear_all()
if utils.USE_THP and excluded is not None:
# If we want to keep THP connection alive, we do not clear communication keys
cache_thp.clear_all_except_one_session_keys(excluded)
else:
_PROTOCOL_CACHE.clear_all()
def get_int_all_sessions(key: int) -> builtins.set[int]: def get_int_all_sessions(key: int) -> builtins.set[int]:

View File

@ -14,6 +14,14 @@ if not utils.BITCOIN_ONLY:
APP_CARDANO_ICARUS_TREZOR_SECRET = const(6) APP_CARDANO_ICARUS_TREZOR_SECRET = const(6)
APP_MONERO_LIVE_REFRESH = const(7) APP_MONERO_LIVE_REFRESH = const(7)
# Cache keys for THP channel
if utils.USE_THP:
CHANNEL_HANDSHAKE_HASH = const(0)
CHANNEL_KEY_RECEIVE = const(1)
CHANNEL_KEY_SEND = const(2)
CHANNEL_NONCE_RECEIVE = const(3)
CHANNEL_NONCE_SEND = const(4)
# Keys that are valid across sessions # Keys that are valid across sessions
SESSIONLESS_FLAG = const(128) SESSIONLESS_FLAG = const(128)
APP_COMMON_SEED_WITHOUT_PASSPHRASE = const(0 | SESSIONLESS_FLAG) APP_COMMON_SEED_WITHOUT_PASSPHRASE = const(0 | SESSIONLESS_FLAG)

View File

@ -0,0 +1,356 @@
import builtins
from micropython import const
from typing import TYPE_CHECKING
from storage.cache_common import DataCache
if TYPE_CHECKING:
from typing import Tuple
pass
# THP specific constants
_MAX_CHANNELS_COUNT = const(10)
_MAX_SESSIONS_COUNT = const(20)
_CHANNEL_STATE_LENGTH = const(1)
_WIRE_INTERFACE_LENGTH = const(1)
_SESSION_STATE_LENGTH = const(1)
_CHANNEL_ID_LENGTH = const(2)
SESSION_ID_LENGTH = const(1)
BROADCAST_CHANNEL_ID = const(0xFFFF)
KEY_LENGTH = const(32)
TAG_LENGTH = const(16)
_UNALLOCATED_STATE = const(0)
_ALLOCATED_STATE = const(1)
_SEEDLESS_STATE = const(2)
class ThpDataCache(DataCache):
def __init__(self) -> None:
self.channel_id = bytearray(_CHANNEL_ID_LENGTH)
self.last_usage = 0
super().__init__()
def clear(self) -> None:
self.channel_id[:] = b""
self.last_usage = 0
super().clear()
class ChannelCache(ThpDataCache):
def __init__(self) -> None:
self.host_ephemeral_pubkey = bytearray(KEY_LENGTH)
self.state = bytearray(_CHANNEL_STATE_LENGTH)
self.iface = bytearray(1) # TODO add decoding
self.sync = 0x80 # can_send_bit | sync_receive_bit | sync_send_bit | rfu(5)
self.session_id_counter = 0x00
self.fields = (
32, # CHANNEL_HANDSHAKE_HASH
32, # CHANNEL_KEY_RECEIVE
32, # CHANNEL_KEY_SEND
8, # CHANNEL_NONCE_RECEIVE
8, # CHANNEL_NONCE_SEND
)
super().__init__()
def clear(self) -> None:
self.state[:] = bytearray(
int.to_bytes(0, _CHANNEL_STATE_LENGTH, "big")
) # Set state to UNALLOCATED
self.host_ephemeral_pubkey[:] = bytearray(KEY_LENGTH)
self.state[:] = bytearray(_CHANNEL_STATE_LENGTH)
self.iface[:] = bytearray(1)
super().clear()
class SessionThpCache(ThpDataCache):
def __init__(self) -> None:
from trezor import utils
self.session_id = bytearray(SESSION_ID_LENGTH)
self.state = bytearray(_SESSION_STATE_LENGTH)
if utils.BITCOIN_ONLY:
self.fields = (
64, # APP_COMMON_SEED
2, # APP_COMMON_AUTHORIZATION_TYPE
128, # APP_COMMON_AUTHORIZATION_DATA
32, # APP_COMMON_NONCE
)
else:
self.fields = (
64, # APP_COMMON_SEED
2, # APP_COMMON_AUTHORIZATION_TYPE
128, # APP_COMMON_AUTHORIZATION_DATA
32, # APP_COMMON_NONCE
0, # APP_COMMON_DERIVE_CARDANO
96, # APP_CARDANO_ICARUS_SECRET
96, # APP_CARDANO_ICARUS_TREZOR_SECRET
0, # APP_MONERO_LIVE_REFRESH
)
super().__init__()
def clear(self) -> None:
self.state[:] = bytearray(
int.to_bytes(0, _SESSION_STATE_LENGTH, "big")
) # Set state to UNALLOCATED
self.session_id[:] = b""
super().clear()
_CHANNELS: list[ChannelCache] = []
_SESSIONS: list[SessionThpCache] = []
cid_counter: int = 0
# Last-used counter
_usage_counter = 0
def initialize() -> None:
global _CHANNELS
global _SESSIONS
global cid_counter
for _ in range(_MAX_CHANNELS_COUNT):
_CHANNELS.append(ChannelCache())
for _ in range(_MAX_SESSIONS_COUNT):
_SESSIONS.append(SessionThpCache())
for channel in _CHANNELS:
channel.clear()
for session in _SESSIONS:
session.clear()
from trezorcrypto import random
cid_counter = random.uniform(0xFFFE)
def get_new_channel(iface: bytes) -> ChannelCache:
if len(iface) != _WIRE_INTERFACE_LENGTH:
raise Exception("Invalid WireInterface (encoded) length")
new_cid = get_next_channel_id()
index = _get_next_channel_index()
# clear sessions from replaced channel
if _get_channel_state(_CHANNELS[index]) != _UNALLOCATED_STATE:
old_cid = _CHANNELS[index].channel_id
clear_sessions_with_channel_id(old_cid)
_CHANNELS[index] = ChannelCache()
_CHANNELS[index].channel_id[:] = new_cid
_CHANNELS[index].last_usage = _get_usage_counter_and_increment()
_CHANNELS[index].state[:] = bytearray(
_UNALLOCATED_STATE.to_bytes(_CHANNEL_STATE_LENGTH, "big")
)
_CHANNELS[index].iface[:] = bytearray(iface)
return _CHANNELS[index]
def update_channel_last_used(channel_id: bytes) -> None:
for channel in _CHANNELS:
if channel.channel_id == channel_id:
channel.last_usage = _get_usage_counter_and_increment()
return
def update_session_last_used(channel_id: bytes, session_id: bytes) -> None:
for session in _SESSIONS:
if session.channel_id == channel_id and session.session_id == session_id:
session.last_usage = _get_usage_counter_and_increment()
update_channel_last_used(channel_id)
return
def get_all_allocated_channels() -> list[ChannelCache]:
_list: list[ChannelCache] = []
for channel in _CHANNELS:
if _get_channel_state(channel) != _UNALLOCATED_STATE:
_list.append(channel)
return _list
def get_allocated_session(
channel_id: bytes, session_id: bytes
) -> SessionThpCache | None:
index = get_allocated_session_index(channel_id, session_id)
if index is None:
return index
return _SESSIONS[index]
def get_allocated_session_index(channel_id: bytes, session_id: bytes) -> int | None:
"""
Finds and returns index of the first allocated session matching the given `channel_id`
and `session_id`, or `None` if no match is found.
Raises `Exception` if either channel_id or session_id has an invalid length.
"""
if len(channel_id) != _CHANNEL_ID_LENGTH or len(session_id) != SESSION_ID_LENGTH:
raise Exception("At least one of arguments has invalid length")
for i in range(_MAX_SESSIONS_COUNT):
if _get_session_state(_SESSIONS[i]) == _UNALLOCATED_STATE:
continue
if _SESSIONS[i].channel_id != channel_id:
continue
if _SESSIONS[i].session_id != session_id:
continue
return i
return None
def is_seedless_session(session_cache: SessionThpCache) -> bool:
return _get_session_state(session_cache) == _SEEDLESS_STATE
def set_channel_host_ephemeral_key(channel: ChannelCache, key: bytearray) -> None:
if len(key) != KEY_LENGTH:
raise Exception("Invalid key length")
channel.host_ephemeral_pubkey = key
def create_or_replace_session(
channel: ChannelCache, session_id: bytes
) -> SessionThpCache:
index = get_allocated_session_index(channel.channel_id, session_id)
if index is None:
index = _get_next_session_index()
_SESSIONS[index] = SessionThpCache()
_SESSIONS[index].channel_id[:] = channel.channel_id
_SESSIONS[index].session_id[:] = session_id
_SESSIONS[index].last_usage = _get_usage_counter_and_increment()
channel.last_usage = (
_get_usage_counter_and_increment()
) # increment also use of the channel so it does not get replaced
_SESSIONS[index].state[:] = bytearray(
_ALLOCATED_STATE.to_bytes(_SESSION_STATE_LENGTH, "big")
)
return _SESSIONS[index]
def _get_usage_counter_and_increment() -> int:
global _usage_counter
_usage_counter += 1
return _usage_counter
def _get_next_channel_index() -> int:
idx = _get_unallocated_channel_index()
if idx is not None:
return idx
return _get_least_recently_used_item(_CHANNELS, max_count=_MAX_CHANNELS_COUNT)
def _get_next_session_index() -> int:
idx = _get_unallocated_session_index()
if idx is not None:
return idx
return _get_least_recently_used_item(_SESSIONS, max_count=_MAX_SESSIONS_COUNT)
def _get_unallocated_channel_index() -> int | None:
for i in range(_MAX_CHANNELS_COUNT):
if _get_channel_state(_CHANNELS[i]) is _UNALLOCATED_STATE:
return i
return None
def _get_unallocated_session_index() -> int | None:
for i in range(_MAX_SESSIONS_COUNT):
if (_SESSIONS[i]) is _UNALLOCATED_STATE:
return i
return None
def _get_channel_state(channel: ChannelCache) -> int:
return int.from_bytes(channel.state, "big")
def _get_session_state(session: SessionThpCache) -> int:
return int.from_bytes(session.state, "big")
def get_next_channel_id() -> bytes:
global cid_counter
while True:
cid_counter += 1
if cid_counter >= BROADCAST_CHANNEL_ID:
cid_counter = 1
if _is_cid_unique():
break
return cid_counter.to_bytes(_CHANNEL_ID_LENGTH, "big")
def _is_cid_unique() -> bool:
global cid_counter
cid_counter_bytes = cid_counter.to_bytes(_CHANNEL_ID_LENGTH, "big")
for channel in _CHANNELS:
if channel.channel_id == cid_counter_bytes:
return False
return True
def _get_least_recently_used_item(
list: list[ChannelCache] | list[SessionThpCache], max_count: int
) -> int:
global _usage_counter
lru_counter = _usage_counter + 1
lru_item_index = 0
for i in range(max_count):
if list[i].last_usage < lru_counter:
lru_counter = list[i].last_usage
lru_item_index = i
return lru_item_index
def get_int_all_sessions(key: int) -> builtins.set[int]:
values = builtins.set()
for session in _SESSIONS:
encoded = session.get(key)
if encoded is not None:
values.add(int.from_bytes(encoded, "big"))
return values
def clear_sessions_with_channel_id(channel_id: bytes) -> None:
for session in _SESSIONS:
if session.channel_id == channel_id:
session.clear()
def clear_session(session: SessionThpCache) -> None:
for s in _SESSIONS:
if s.channel_id == session.channel_id and s.session_id == session.session_id:
session.clear()
def clear_all() -> None:
for session in _SESSIONS:
session.clear()
for channel in _CHANNELS:
channel.clear()
def clear_all_except_one_session_keys(excluded: Tuple[bytes, bytes]) -> None:
cid, sid = excluded
for channel in _CHANNELS:
if channel.channel_id != cid:
channel.clear()
for session in _SESSIONS:
if session.channel_id != cid and session.session_id != sid:
session.clear()
else:
s_last_usage = session.last_usage
session.clear()
session.last_usage = s_last_usage
session.state = bytearray(_SEEDLESS_STATE.to_bytes(1, "big"))
session.session_id[:] = bytearray(sid)
session.channel_id[:] = bytearray(cid)

View File

@ -16,4 +16,8 @@ NotInitialized = 11
PinMismatch = 12 PinMismatch = 12
WipeCodeMismatch = 13 WipeCodeMismatch = 13
InvalidSession = 14 InvalidSession = 14
ThpUnallocatedSession = 15
InvalidProtocol = 16
BufferError = 17
DeviceIsBusy = 18
FirmwareError = 99 FirmwareError = 99

View File

@ -97,6 +97,8 @@ DebugLinkEraseSdCard = 9005
DebugLinkWatchLayout = 9006 DebugLinkWatchLayout = 9006
DebugLinkResetDebugEvents = 9007 DebugLinkResetDebugEvents = 9007
DebugLinkOptigaSetSecMax = 9008 DebugLinkOptigaSetSecMax = 9008
DebugLinkGetPairingInfo = 9009
DebugLinkPairingInfo = 9010
BenchmarkListNames = 9100 BenchmarkListNames = 9100
BenchmarkNames = 9101 BenchmarkNames = 9101
BenchmarkRun = 9102 BenchmarkRun = 9102

22
core/src/trezor/enums/ThpMessageType.py generated Normal file
View File

@ -0,0 +1,22 @@
# Automatically generated by pb2py
# fmt: off
# isort:skip_file
ThpCreateNewSession = 1000
ThpPairingRequest = 1006
ThpPairingRequestApproved = 1007
ThpSelectMethod = 1008
ThpPairingPreparationsFinished = 1009
ThpCredentialRequest = 1010
ThpCredentialResponse = 1011
ThpEndRequest = 1012
ThpEndResponse = 1013
ThpCodeEntryCommitment = 1016
ThpCodeEntryChallenge = 1017
ThpCodeEntryCpaceTrezor = 1018
ThpCodeEntryCpaceHostTag = 1019
ThpCodeEntrySecret = 1020
ThpQrCodeTag = 1024
ThpQrCodeSecret = 1025
ThpNfcTagHost = 1032
ThpNfcTagTrezor = 1033

View File

@ -0,0 +1,8 @@
# Automatically generated by pb2py
# fmt: off
# isort:skip_file
SkipPairing = 1
CodeEntry = 2
QrCode = 3
NFC = 4

View File

@ -39,6 +39,10 @@ if TYPE_CHECKING:
PinMismatch = 12 PinMismatch = 12
WipeCodeMismatch = 13 WipeCodeMismatch = 13
InvalidSession = 14 InvalidSession = 14
ThpUnallocatedSession = 15
InvalidProtocol = 16
BufferError = 17
DeviceIsBusy = 18
FirmwareError = 99 FirmwareError = 99
class ButtonRequestType(IntEnum): class ButtonRequestType(IntEnum):
@ -347,6 +351,32 @@ if TYPE_CHECKING:
Nay = 1 Nay = 1
Pass = 2 Pass = 2
class ThpMessageType(IntEnum):
ThpCreateNewSession = 1000
ThpPairingRequest = 1006
ThpPairingRequestApproved = 1007
ThpSelectMethod = 1008
ThpPairingPreparationsFinished = 1009
ThpCredentialRequest = 1010
ThpCredentialResponse = 1011
ThpEndRequest = 1012
ThpEndResponse = 1013
ThpCodeEntryCommitment = 1016
ThpCodeEntryChallenge = 1017
ThpCodeEntryCpaceTrezor = 1018
ThpCodeEntryCpaceHostTag = 1019
ThpCodeEntrySecret = 1020
ThpQrCodeTag = 1024
ThpQrCodeSecret = 1025
ThpNfcTagHost = 1032
ThpNfcTagTrezor = 1033
class ThpPairingMethod(IntEnum):
SkipPairing = 1
CodeEntry = 2
QrCode = 3
NFC = 4
class MessageType(IntEnum): class MessageType(IntEnum):
Initialize = 0 Initialize = 0
Ping = 1 Ping = 1
@ -447,6 +477,8 @@ if TYPE_CHECKING:
DebugLinkWatchLayout = 9006 DebugLinkWatchLayout = 9006
DebugLinkResetDebugEvents = 9007 DebugLinkResetDebugEvents = 9007
DebugLinkOptigaSetSecMax = 9008 DebugLinkOptigaSetSecMax = 9008
DebugLinkGetPairingInfo = 9009
DebugLinkPairingInfo = 9010
EthereumGetPublicKey = 450 EthereumGetPublicKey = 450
EthereumPublicKey = 451 EthereumPublicKey = 451
EthereumGetAddress = 56 EthereumGetAddress = 56

View File

@ -68,6 +68,8 @@ if TYPE_CHECKING:
from trezor.enums import StellarSignerType # noqa: F401 from trezor.enums import StellarSignerType # noqa: F401
from trezor.enums import TezosBallotType # noqa: F401 from trezor.enums import TezosBallotType # noqa: F401
from trezor.enums import TezosContractType # noqa: F401 from trezor.enums import TezosContractType # noqa: F401
from trezor.enums import ThpMessageType # noqa: F401
from trezor.enums import ThpPairingMethod # noqa: F401
from trezor.enums import WordRequestType # noqa: F401 from trezor.enums import WordRequestType # noqa: F401
class BenchmarkListNames(protobuf.MessageType): class BenchmarkListNames(protobuf.MessageType):
@ -2950,6 +2952,46 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkState"]: def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkState"]:
return isinstance(msg, cls) return isinstance(msg, cls)
class DebugLinkGetPairingInfo(protobuf.MessageType):
channel_id: "bytes | None"
handshake_hash: "bytes | None"
nfc_secret_host: "bytes | None"
def __init__(
self,
*,
channel_id: "bytes | None" = None,
handshake_hash: "bytes | None" = None,
nfc_secret_host: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkGetPairingInfo"]:
return isinstance(msg, cls)
class DebugLinkPairingInfo(protobuf.MessageType):
channel_id: "bytes | None"
handshake_hash: "bytes | None"
code_entry_code: "int | None"
code_qr_code: "bytes | None"
nfc_secret_trezor: "bytes | None"
def __init__(
self,
*,
channel_id: "bytes | None" = None,
handshake_hash: "bytes | None" = None,
code_entry_code: "int | None" = None,
code_qr_code: "bytes | None" = None,
nfc_secret_trezor: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkPairingInfo"]:
return isinstance(msg, cls)
class DebugLinkStop(protobuf.MessageType): class DebugLinkStop(protobuf.MessageType):
@classmethod @classmethod
@ -6164,7 +6206,61 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["TezosManagerTransfer"]: def is_type_of(cls, msg: Any) -> TypeGuard["TezosManagerTransfer"]:
return isinstance(msg, cls) return isinstance(msg, cls)
class ThpCredentialMetadata(protobuf.MessageType): class ThpDeviceProperties(protobuf.MessageType):
internal_model: "str | None"
model_variant: "int | None"
protocol_version_major: "int | None"
protocol_version_minor: "int | None"
pairing_methods: "list[ThpPairingMethod]"
def __init__(
self,
*,
pairing_methods: "list[ThpPairingMethod] | None" = None,
internal_model: "str | None" = None,
model_variant: "int | None" = None,
protocol_version_major: "int | None" = None,
protocol_version_minor: "int | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpDeviceProperties"]:
return isinstance(msg, cls)
class ThpHandshakeCompletionReqNoisePayload(protobuf.MessageType):
host_pairing_credential: "bytes | None"
def __init__(
self,
*,
host_pairing_credential: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpHandshakeCompletionReqNoisePayload"]:
return isinstance(msg, cls)
class ThpCreateNewSession(protobuf.MessageType):
passphrase: "str | None"
on_device: "bool | None"
derive_cardano: "bool | None"
def __init__(
self,
*,
passphrase: "str | None" = None,
on_device: "bool | None" = None,
derive_cardano: "bool | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCreateNewSession"]:
return isinstance(msg, cls)
class ThpPairingRequest(protobuf.MessageType):
host_name: "str | None" host_name: "str | None"
def __init__( def __init__(
@ -6174,6 +6270,220 @@ if TYPE_CHECKING:
) -> None: ) -> None:
pass pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpPairingRequest"]:
return isinstance(msg, cls)
class ThpPairingRequestApproved(protobuf.MessageType):
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpPairingRequestApproved"]:
return isinstance(msg, cls)
class ThpSelectMethod(protobuf.MessageType):
selected_pairing_method: "ThpPairingMethod | None"
def __init__(
self,
*,
selected_pairing_method: "ThpPairingMethod | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpSelectMethod"]:
return isinstance(msg, cls)
class ThpPairingPreparationsFinished(protobuf.MessageType):
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpPairingPreparationsFinished"]:
return isinstance(msg, cls)
class ThpCodeEntryCommitment(protobuf.MessageType):
commitment: "bytes | None"
def __init__(
self,
*,
commitment: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCodeEntryCommitment"]:
return isinstance(msg, cls)
class ThpCodeEntryChallenge(protobuf.MessageType):
challenge: "bytes | None"
def __init__(
self,
*,
challenge: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCodeEntryChallenge"]:
return isinstance(msg, cls)
class ThpCodeEntryCpaceTrezor(protobuf.MessageType):
cpace_trezor_public_key: "bytes | None"
def __init__(
self,
*,
cpace_trezor_public_key: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCodeEntryCpaceTrezor"]:
return isinstance(msg, cls)
class ThpCodeEntryCpaceHostTag(protobuf.MessageType):
cpace_host_public_key: "bytes | None"
tag: "bytes | None"
def __init__(
self,
*,
cpace_host_public_key: "bytes | None" = None,
tag: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCodeEntryCpaceHostTag"]:
return isinstance(msg, cls)
class ThpCodeEntrySecret(protobuf.MessageType):
secret: "bytes | None"
def __init__(
self,
*,
secret: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCodeEntrySecret"]:
return isinstance(msg, cls)
class ThpQrCodeTag(protobuf.MessageType):
tag: "bytes | None"
def __init__(
self,
*,
tag: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpQrCodeTag"]:
return isinstance(msg, cls)
class ThpQrCodeSecret(protobuf.MessageType):
secret: "bytes | None"
def __init__(
self,
*,
secret: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpQrCodeSecret"]:
return isinstance(msg, cls)
class ThpNfcTagHost(protobuf.MessageType):
tag: "bytes | None"
def __init__(
self,
*,
tag: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpNfcTagHost"]:
return isinstance(msg, cls)
class ThpNfcTagTrezor(protobuf.MessageType):
tag: "bytes | None"
def __init__(
self,
*,
tag: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpNfcTagTrezor"]:
return isinstance(msg, cls)
class ThpCredentialRequest(protobuf.MessageType):
host_static_pubkey: "bytes | None"
autoconnect: "bool | None"
def __init__(
self,
*,
host_static_pubkey: "bytes | None" = None,
autoconnect: "bool | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCredentialRequest"]:
return isinstance(msg, cls)
class ThpCredentialResponse(protobuf.MessageType):
trezor_static_pubkey: "bytes | None"
credential: "bytes | None"
def __init__(
self,
*,
trezor_static_pubkey: "bytes | None" = None,
credential: "bytes | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCredentialResponse"]:
return isinstance(msg, cls)
class ThpEndRequest(protobuf.MessageType):
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpEndRequest"]:
return isinstance(msg, cls)
class ThpEndResponse(protobuf.MessageType):
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpEndResponse"]:
return isinstance(msg, cls)
class ThpCredentialMetadata(protobuf.MessageType):
host_name: "str | None"
autoconnect: "bool | None"
def __init__(
self,
*,
host_name: "str | None" = None,
autoconnect: "bool | None" = None,
) -> None:
pass
@classmethod @classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCredentialMetadata"]: def is_type_of(cls, msg: Any) -> TypeGuard["ThpCredentialMetadata"]:
return isinstance(msg, cls) return isinstance(msg, cls)

View File

@ -35,6 +35,10 @@ from trezorutils import ( # noqa: F401
) )
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
DISABLE_ENCRYPTION: bool = False
ALLOW_DEBUG_MESSAGES: bool = True
if __debug__: if __debug__:
if EMULATOR: if EMULATOR:
import uos import uos

View File

@ -5,7 +5,7 @@ Handles on-the-wire communication with a host computer. The communication is:
- Request / response. - Request / response.
- Protobuf-encoded, see `protobuf.py`. - Protobuf-encoded, see `protobuf.py`.
- Wrapped in a simple envelope format, see `trezor/wire/codec/codec_v1.py`. - Wrapped in a simple envelope format, see `trezor/wire/codec/codec_v1.py` or `trezor/wire/thp/thp_main.py`.
- Transferred over USB interface, or UDP in case of Unix emulation. - Transferred over USB interface, or UDP in case of Unix emulation.
This module: This module:
@ -29,7 +29,12 @@ from typing import TYPE_CHECKING
from trezor import log, loop, protobuf, utils from trezor import log, loop, protobuf, utils
from . import message_handler, protocol_common from . import message_handler, protocol_common
from .codec.codec_context import CodecContext
if utils.USE_THP:
from .thp import thp_main
else:
from .codec.codec_context import CodecContext
from .context import UnexpectedMessageException from .context import UnexpectedMessageException
from .message_handler import failure from .message_handler import failure
@ -37,10 +42,6 @@ from .message_handler import failure
# other packages. # other packages.
from .errors import * # isort:skip # noqa: F401,F403 from .errors import * # isort:skip # noqa: F401,F403
_PROTOBUF_BUFFER_SIZE = const(8192)
WIRE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
if TYPE_CHECKING: if TYPE_CHECKING:
from trezorio import WireInterface from trezorio import WireInterface
from typing import Any, Callable, Coroutine, TypeVar from typing import Any, Callable, Coroutine, TypeVar
@ -57,57 +58,91 @@ def setup(iface: WireInterface) -> None:
loop.schedule(handle_session(iface)) loop.schedule(handle_session(iface))
async def handle_session(iface: WireInterface) -> None: if utils.USE_THP:
ctx = CodecContext(iface, WIRE_BUFFER) # memory_manager is imported to create READ/WRITE buffers
next_msg: protocol_common.Message | None = None # in more stable area of memory
from .thp import memory_manager # noqa: F401
# Take a mark of modules that are imported at this point, so we can async def handle_session(iface: WireInterface) -> None:
# roll back and un-import any others.
modules = utils.unimport_begin()
while True:
try:
if next_msg is None:
# If the previous run did not keep an unprocessed message for us,
# wait for a new one coming from the wire.
try:
msg = await ctx.read_from_wire()
except protocol_common.WireError as exc:
if __debug__:
log.exception(__name__, exc)
await ctx.write(failure(exc))
continue
else: # Take a mark of modules that are imported at this point, so we can
# Process the message from previous run. # roll back and un-import any others.
msg = next_msg modules = utils.unimport_begin()
next_msg = None
do_not_restart = False while True:
try: try:
do_not_restart = await message_handler.handle_single_message(ctx, msg) await thp_main.thp_main_loop(iface)
except UnexpectedMessageException as unexpected:
# The workflow was interrupted by an unexpected message. We need to
# process it as if it was a new message...
next_msg = unexpected.msg
# ...and we must not restart because that would lose the message.
do_not_restart = True
continue
except Exception as exc: except Exception as exc:
# Log and ignore. The session handler can only exit explicitly in the # Log and try again.
# following finally block.
if __debug__: if __debug__:
log.exception(__name__, exc) log.exception(__name__, exc)
finally: finally:
# Unload modules imported by the workflow. Should not raise. # Unload modules imported by the workflow. Should not raise.
if __debug__:
log.debug(__name__, "utils.unimport_end(modules) and loop.clear()")
utils.unimport_end(modules) utils.unimport_end(modules)
loop.clear()
return # pylint: disable=lost-exception
if not do_not_restart: else:
# Let the session be restarted from `main`. _PROTOBUF_BUFFER_SIZE = const(8192)
loop.clear() WIRE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
return # pylint: disable=lost-exception
except Exception as exc: async def handle_session(iface: WireInterface) -> None:
# Log and try again. The session handler can only exit explicitly via ctx = CodecContext(iface, WIRE_BUFFER)
# loop.clear() above. next_msg: protocol_common.Message | None = None
if __debug__:
log.exception(__name__, exc) # Take a mark of modules that are imported at this point, so we can
# roll back and un-import any others.
modules = utils.unimport_begin()
while True:
try:
if next_msg is None:
# If the previous run did not keep an unprocessed message for us,
# wait for a new one coming from the wire.
try:
msg = await ctx.read_from_wire()
except protocol_common.WireError as exc:
if __debug__:
log.exception(__name__, exc)
await ctx.write(failure(exc))
continue
else:
# Process the message from previous run.
msg = next_msg
next_msg = None
do_not_restart = False
try:
do_not_restart = await message_handler.handle_single_message(
ctx, msg
)
except UnexpectedMessageException as unexpected:
# The workflow was interrupted by an unexpected message. We need to
# process it as if it was a new message...
next_msg = unexpected.msg
# ...and we must not restart because that would lose the message.
do_not_restart = True
continue
except Exception as exc:
# Log and ignore. The session handler can only exit explicitly in the
# following finally block.
if __debug__:
log.exception(__name__, exc)
finally:
# Unload modules imported by the workflow. Should not raise.
utils.unimport_end(modules)
if not do_not_restart:
# Let the session be restarted from `main`.
if __debug__:
log.debug(__name__, "loop.clear()")
loop.clear()
return # pylint: disable=lost-exception
except Exception as exc:
# Log and try again. The session handler can only exit explicitly via
# loop.clear() above.
if __debug__:
log.exception(__name__, exc)

View File

@ -17,7 +17,7 @@ from typing import TYPE_CHECKING
from storage import cache from storage import cache
from storage.cache_common import SESSIONLESS_FLAG from storage.cache_common import SESSIONLESS_FLAG
from trezor import loop, protobuf from trezor import loop, protobuf, utils
from .protocol_common import Context, Message from .protocol_common import Context, Message
@ -138,6 +138,17 @@ def with_context(ctx: Context, workflow: loop.Task) -> Generator:
send_exc = None send_exc = None
def try_get_ctx_ids() -> tuple[bytes, bytes] | None:
ids = None
if utils.USE_THP:
from trezor.wire.thp.session_context import GenericSessionContext
ctx = get_context()
if isinstance(ctx, GenericSessionContext):
ids = (ctx.channel_id, ctx.session_id.to_bytes(1, "big"))
return ids
# ACCESS TO CACHE # ACCESS TO CACHE
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -8,6 +8,17 @@ class Error(Exception):
self.message = message self.message = message
class SilentError(Exception):
def __init__(self, message: str) -> None:
super().__init__()
self.message = message
class WireBufferError(Error):
def __init__(self, message: str = "Buffer error") -> None:
super().__init__(FailureType.BufferError, message)
class UnexpectedMessage(Error): class UnexpectedMessage(Error):
def __init__(self, message: str) -> None: def __init__(self, message: str) -> None:
super().__init__(FailureType.UnexpectedMessage, message) super().__init__(FailureType.UnexpectedMessage, message)

View File

@ -25,7 +25,12 @@ def wrap_protobuf_load(
expected_type: type[LoadedMessageType], expected_type: type[LoadedMessageType],
) -> LoadedMessageType: ) -> LoadedMessageType:
try: try:
if __debug__ and utils.EMULATOR and utils.USE_THP: if (
__debug__
and utils.EMULATOR
and utils.USE_THP
and utils.ALLOW_DEBUG_MESSAGES
):
log.debug( log.debug(
__name__, __name__,
"Buffer to be parsed to a LoadedMessage: %s", "Buffer to be parsed to a LoadedMessage: %s",
@ -38,7 +43,7 @@ def wrap_protobuf_load(
) )
return msg return msg
except Exception as e: except Exception as e:
if __debug__: if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.exception(__name__, e) log.exception(__name__, e)
if e.args: if e.args:
raise DataError("Failed to decode message: " + " ".join(e.args)) raise DataError("Failed to decode message: " + " ".join(e.args))
@ -46,6 +51,25 @@ def wrap_protobuf_load(
raise DataError("Failed to decode message") raise DataError("Failed to decode message")
if utils.USE_THP:
from trezor.enums import ThpMessageType
def get_msg_name(msg_type: int) -> str | None:
for name in dir(ThpMessageType):
if not name.startswith("__"): # Skip built-in attributes
value = getattr(ThpMessageType, name)
if isinstance(value, int):
if value == msg_type:
return name
return None
def get_msg_type(msg_name: str) -> int | None:
value = getattr(ThpMessageType, msg_name)
if isinstance(value, int):
return value
return None
async def handle_single_message(ctx: Context, msg: Message) -> bool: async def handle_single_message(ctx: Context, msg: Message) -> bool:
"""Handle a message that was loaded from a WireInterface by the caller. """Handle a message that was loaded from a WireInterface by the caller.
@ -60,17 +84,27 @@ async def handle_single_message(ctx: Context, msg: Message) -> bool:
the type of message is supposed to be optimized and not disrupt the running state, the type of message is supposed to be optimized and not disrupt the running state,
this function will return `True`. this function will return `True`.
""" """
if __debug__: if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
try: try:
msg_type = protobuf.type_for_wire(msg.type).MESSAGE_NAME msg_type = protobuf.type_for_wire(msg.type).MESSAGE_NAME
except Exception: except Exception:
msg_type = f"{msg.type} - unknown message type" msg_type = f"{msg.type} - unknown message type"
log.debug( if utils.USE_THP:
__name__, cid = int.from_bytes(ctx.channel_id, "big")
"%d receive: <%s>", log.debug(
ctx.iface.iface_num(), __name__,
msg_type, "%d:%d receive: <%s>",
) ctx.iface.iface_num(),
cid,
msg_type,
)
else:
log.debug(
__name__,
"%d receive: <%s>",
ctx.iface.iface_num(),
msg_type,
)
res_msg: protobuf.MessageType | None = None res_msg: protobuf.MessageType | None = None
@ -132,7 +166,7 @@ async def handle_single_message(ctx: Context, msg: Message) -> bool:
# - the message was not valid protobuf # - the message was not valid protobuf
# - workflow raised some kind of an exception while running # - workflow raised some kind of an exception while running
# - something canceled the workflow from the outside # - something canceled the workflow from the outside
if __debug__: if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
if isinstance(exc, ActionCancelled): if isinstance(exc, ActionCancelled):
log.debug(__name__, "cancelled: %s", exc.message) log.debug(__name__, "cancelled: %s", exc.message)
elif isinstance(exc, loop.TaskClosed): elif isinstance(exc, loop.TaskClosed):

View File

@ -4,7 +4,7 @@ from trezor import protobuf
if TYPE_CHECKING: if TYPE_CHECKING:
from trezorio import WireInterface from trezorio import WireInterface
from typing import Container, TypeVar, overload from typing import Awaitable, Container, TypeVar, overload
from storage.cache_common import DataCache from storage.cache_common import DataCache
@ -72,6 +72,9 @@ class Context:
"""Write a message to the wire.""" """Write a message to the wire."""
... ...
def write_force(self, msg: protobuf.MessageType) -> Awaitable[None]:
return self.write(msg)
async def call( async def call(
self, self,
msg: protobuf.MessageType, msg: protobuf.MessageType,

View File

@ -0,0 +1,189 @@
import ustruct
from micropython import const
from typing import TYPE_CHECKING
from storage.cache_thp import BROADCAST_CHANNEL_ID
from trezor import protobuf, utils
from trezor.enums import ThpPairingMethod
from trezor.messages import ThpDeviceProperties
from ..protocol_common import WireError
if TYPE_CHECKING:
from enum import IntEnum
from trezor.wire import WireInterface
from typing_extensions import Self
else:
IntEnum = object
CODEC_V1 = const(0x3F)
HANDSHAKE_INIT_REQ = const(0x00)
HANDSHAKE_INIT_RES = const(0x01)
HANDSHAKE_COMP_REQ = const(0x02)
HANDSHAKE_COMP_RES = const(0x03)
ENCRYPTED = const(0x04)
ACK_MESSAGE = const(0x20)
CHANNEL_ALLOCATION_REQ = const(0x40)
_CHANNEL_ALLOCATION_RES = const(0x41)
_ERROR = const(0x42)
CONTINUATION_PACKET = const(0x80)
class ThpError(WireError):
pass
class ThpDecryptionError(ThpError):
pass
class ThpInvalidDataError(ThpError):
pass
class ThpDeviceLockedError(ThpError):
pass
class ThpUnallocatedSessionError(ThpError):
def __init__(self, session_id: int) -> None:
self.session_id = session_id
class ThpErrorType(IntEnum):
TRANSPORT_BUSY = 1
UNALLOCATED_CHANNEL = 2
DECRYPTION_FAILED = 3
INVALID_DATA = 4
DEVICE_LOCKED = 5
class ChannelState(IntEnum):
UNALLOCATED = 0
TH1 = 1
TH2 = 2
TP0 = 3
TP1 = 4
TP2 = 5
TP3 = 6
TP4 = 7
TC1 = 8
ENCRYPTED_TRANSPORT = 9
INVALIDATED = 10
class SessionState(IntEnum):
UNALLOCATED = 0
ALLOCATED = 1
SEEDLESS = 2
class PacketHeader:
format_str_init = ">BHH"
format_str_cont = ">BH"
def __init__(self, ctrl_byte: int, cid: int, length: int) -> None:
self.ctrl_byte = ctrl_byte
self.cid = cid
self.length = length
def to_bytes(self) -> bytes:
return ustruct.pack(self.format_str_init, self.ctrl_byte, self.cid, self.length)
def pack_to_init_buffer(self, buffer: bytearray, buffer_offset: int = 0) -> None:
"""
Packs header information in the form of **intial** packet
into the provided buffer.
"""
ustruct.pack_into(
self.format_str_init,
buffer,
buffer_offset,
self.ctrl_byte,
self.cid,
self.length,
)
def pack_to_cont_buffer(self, buffer: bytearray, buffer_offset: int = 0) -> None:
"""
Packs header information in the form of **continuation** packet header
into the provided buffer.
"""
ustruct.pack_into(
self.format_str_cont, buffer, buffer_offset, CONTINUATION_PACKET, self.cid
)
@classmethod
def get_error_header(cls, cid: int, length: int) -> Self:
"""
Returns header for protocol-level error messages.
"""
return cls(_ERROR, cid, length)
@classmethod
def get_channel_allocation_response_header(cls, length: int) -> Self:
"""
Returns header for allocation response handshake message.
"""
return cls(_CHANNEL_ALLOCATION_RES, BROADCAST_CHANNEL_ID, length)
_DEFAULT_ENABLED_PAIRING_METHODS = [
ThpPairingMethod.CodeEntry,
ThpPairingMethod.QrCode,
ThpPairingMethod.NFC,
]
def get_enabled_pairing_methods(
iface: WireInterface | None = None,
) -> list[ThpPairingMethod]:
"""
Returns pairing methods that are currently allowed by the device
with respect to the wire interface the host communicates on.
"""
methods = _DEFAULT_ENABLED_PAIRING_METHODS.copy()
if __debug__:
methods.append(ThpPairingMethod.SkipPairing)
return methods
def _get_device_properties(iface: WireInterface) -> ThpDeviceProperties:
# TODO define model variants
return ThpDeviceProperties(
pairing_methods=get_enabled_pairing_methods(iface),
internal_model=utils.INTERNAL_MODEL,
model_variant=0,
protocol_version_major=2,
protocol_version_minor=0,
)
def get_encoded_device_properties(iface: WireInterface) -> bytes:
props = _get_device_properties(iface)
length = protobuf.encoded_length(props)
encoded_properties = bytearray(length)
protobuf.encode(encoded_properties, props)
return encoded_properties
def get_channel_allocation_response(
nonce: bytes, new_cid: bytes, iface: WireInterface
) -> bytes:
props_msg = get_encoded_device_properties(iface)
return nonce + new_cid + props_msg
if __debug__:
def state_to_str(state: int) -> str:
name = {
v: k for k, v in ChannelState.__dict__.items() if not k.startswith("__")
}.get(state)
if name is not None:
return name
return "UNKNOWN_STATE"

View File

@ -0,0 +1,102 @@
from storage.cache_thp import ChannelCache
from trezor import log, utils
from trezor.wire.thp import ThpError
def is_ack_valid(cache: ChannelCache, ack_bit: int) -> bool:
"""
Checks if:
- an ACK message is expected
- the received ACK message acknowledges correct sequence number (bit)
"""
if not _is_ack_expected(cache):
return False
if not _has_ack_correct_sync_bit(cache, ack_bit):
return False
return True
def _is_ack_expected(cache: ChannelCache) -> bool:
is_expected: bool = not is_sending_allowed(cache)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES and not is_expected:
log.debug(__name__, "Received unexpected ACK message")
return is_expected
def _has_ack_correct_sync_bit(cache: ChannelCache, sync_bit: int) -> bool:
is_correct: bool = get_send_seq_bit(cache) == sync_bit
if __debug__ and utils.ALLOW_DEBUG_MESSAGES and not is_correct:
log.debug(__name__, "Received ACK message with wrong ack bit")
return is_correct
def is_sending_allowed(cache: ChannelCache) -> bool:
"""
Checks whether sending a message in the provided channel is allowed.
Note: Sending a message in a channel before receipt of ACK message for the previously
sent message (in the channel) is prohibited, as it can lead to desynchronization.
"""
return bool(cache.sync >> 7)
def get_send_seq_bit(cache: ChannelCache) -> int:
"""
Returns the sequential number (bit) of the next message to be sent
in the provided channel.
"""
return (cache.sync & 0x20) >> 5
def get_expected_receive_seq_bit(cache: ChannelCache) -> int:
"""
Returns the (expected) sequential number (bit) of the next message
to be received in the provided channel.
"""
return (cache.sync & 0x40) >> 6
def set_sending_allowed(cache: ChannelCache, sending_allowed: bool) -> None:
"""
Set the flag whether sending a message in this channel is allowed or not.
"""
cache.sync &= 0x7F
if sending_allowed:
cache.sync |= 0x80
def set_expected_receive_seq_bit(cache: ChannelCache, seq_bit: int) -> None:
"""
Set the expected sequential number (bit) of the next message to be received
in the provided channel
"""
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "Set sync receive expected seq bit to %d", seq_bit)
if seq_bit not in (0, 1):
raise ThpError("Unexpected receive sync bit")
# set second bit to "seq_bit" value
cache.sync &= 0xBF
if seq_bit:
cache.sync |= 0x40
def _set_send_seq_bit(cache: ChannelCache, seq_bit: int) -> None:
if seq_bit not in (0, 1):
raise ThpError("Unexpected send seq bit")
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "setting sync send seq bit to %d", seq_bit)
# set third bit to "seq_bit" value
cache.sync &= 0xDF
if seq_bit:
cache.sync |= 0x20
def set_send_seq_bit_to_opposite(cache: ChannelCache) -> None:
"""
Set the sequential bit of the "next message to be send" to the opposite value,
i.e. 1 -> 0 and 0 -> 1
"""
_set_send_seq_bit(cache=cache, seq_bit=1 - get_send_seq_bit(cache))

View File

@ -0,0 +1,522 @@
import ustruct
from typing import TYPE_CHECKING
from storage.cache_common import (
CHANNEL_HANDSHAKE_HASH,
CHANNEL_KEY_RECEIVE,
CHANNEL_KEY_SEND,
CHANNEL_NONCE_RECEIVE,
CHANNEL_NONCE_SEND,
)
from storage.cache_thp import (
SESSION_ID_LENGTH,
TAG_LENGTH,
ChannelCache,
clear_sessions_with_channel_id,
)
from trezor import log, loop, protobuf, utils, workflow
from trezor.wire.errors import WireBufferError
from . import ENCRYPTED, ChannelState, PacketHeader, ThpDecryptionError, ThpError
from . import alternating_bit_protocol as ABP
from . import (
checksum,
control_byte,
crypto,
interface_manager,
memory_manager,
received_message_handler,
)
from .checksum import CHECKSUM_LENGTH
from .transmission_loop import TransmissionLoop
from .writer import (
CONT_HEADER_LENGTH,
INIT_HEADER_LENGTH,
MESSAGE_TYPE_LENGTH,
write_payload_to_wire_and_add_checksum,
)
if __debug__:
from trezor.utils import get_bytes_as_str
from . import state_to_str
if TYPE_CHECKING:
from trezorio import WireInterface
from typing import Awaitable
from trezor.messages import ThpPairingCredential
from .pairing_context import PairingContext
from .session_context import GenericSessionContext
class Channel:
"""
THP protocol encrypted communication channel.
"""
def __init__(self, channel_cache: ChannelCache) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "channel initialization")
# Channel properties
self.iface: WireInterface = interface_manager.decode_iface(channel_cache.iface)
self.channel_cache: ChannelCache = channel_cache
self.channel_id: bytes = channel_cache.channel_id
# Shared variables
self.buffer: utils.BufferType = bytearray(self.iface.TX_PACKET_LEN)
self.fallback_decrypt: bool = False
self.bytes_read: int = 0
self.expected_payload_length: int = 0
self.is_cont_packet_expected: bool = False
self.sessions: dict[int, GenericSessionContext] = {}
# Objects for writing a message to a wire
self.transmission_loop: TransmissionLoop | None = None
self.write_task_spawn: loop.spawn | None = None
# Temporary objects
self.handshake: crypto.Handshake | None = None
self.credential: ThpPairingCredential | None = None
self.connection_context: PairingContext | None = None
self.busy_decoder: crypto.BusyDecoder | None = None
self.temp_crc: int | None = None
self.temp_crc_compare: bytearray | None = None
self.temp_tag: bytearray | None = None
def clear(self) -> None:
clear_sessions_with_channel_id(self.channel_id)
self.channel_cache.clear()
# ACCESS TO CHANNEL_DATA
def get_channel_id_int(self) -> int:
return int.from_bytes(self.channel_id, "big")
def get_channel_state(self) -> int:
state = int.from_bytes(self.channel_cache.state, "big")
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("get_channel_state: ", state_to_str(state))
return state
def get_handshake_hash(self) -> bytes:
h = self.channel_cache.get(CHANNEL_HANDSHAKE_HASH)
assert h is not None
return h
def set_channel_state(self, state: ChannelState) -> None:
self.channel_cache.state = bytearray(state.to_bytes(1, "big"))
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("set_channel_state: ", state_to_str(state))
# READ and DECRYPT
def receive_packet(self, packet: utils.BufferType) -> Awaitable[None] | None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("receive packet")
self._handle_received_packet(packet)
try:
buffer = memory_manager.get_existing_read_buffer(self.get_channel_id_int())
except WireBufferError:
pass # TODO ??
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
try:
self._log("self.buffer: ", get_bytes_as_str(buffer))
except Exception:
pass # TODO handle nicer - happens in fallback_decrypt
if self.expected_payload_length + INIT_HEADER_LENGTH == self.bytes_read:
self._finish_message()
if self.fallback_decrypt:
# TODO Check CRC and if valid, check tag, if valid update nonces
self._finish_fallback()
# TODO self.write() failure device is busy - use channel buffer to send this failure message!!
return None
return received_message_handler.handle_received_message(self, buffer)
elif self.expected_payload_length + INIT_HEADER_LENGTH > self.bytes_read:
self.is_cont_packet_expected = True
else:
raise ThpError(
"Read more bytes than is the expected length of the message!"
)
return None
def _handle_received_packet(self, packet: utils.BufferType) -> None:
ctrl_byte = packet[0]
if control_byte.is_continuation(ctrl_byte):
self._handle_cont_packet(packet)
return
self._handle_init_packet(packet)
def _handle_init_packet(self, packet: utils.BufferType) -> None:
self.fallback_decrypt = False
self.bytes_read = 0
self.expected_payload_length = 0
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("handle_init_packet")
_, _, payload_length = ustruct.unpack(PacketHeader.format_str_init, packet)
self.expected_payload_length = payload_length
# If the channel does not "own" the buffer lock, decrypt first packet
# TODO do it only when needed!
# TODO FIX: If "_decrypt_single_packet_payload" is implemented, it will (possibly) break "decrypt_buffer" and nonces incrementation.
# On the other hand, without the single packet decryption, the "advanced" buffer selection cannot be implemented
# in "memory_manager.select_buffer", because the session id is unknown (encrypted).
# if control_byte.is_encrypted_transport(ctrl_byte):
# packet_payload = self._decrypt_single_packet_payload(packet_payload)
cid = self.get_channel_id_int()
length = payload_length + INIT_HEADER_LENGTH
try:
buffer = memory_manager.get_new_read_buffer(cid, length)
except WireBufferError:
# TODO handle not encrypted/(short??), eg. ACK
self.fallback_decrypt = True
self._prepare_fallback()
to_read_len = min(len(packet) - INIT_HEADER_LENGTH, payload_length)
buf = memoryview(self.buffer)[:to_read_len]
utils.memcpy(buf, 0, packet, INIT_HEADER_LENGTH)
# CRC CHECK
self._handle_fallback_crc(buf)
# TAG CHECK
self._handle_fallback_decryption(buf)
self.bytes_read += to_read_len
return
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("handle_init_packet - payload len: ", str(payload_length))
self._log("handle_init_packet - buffer len: ", str(len(buffer)))
self._buffer_packet_data(buffer, packet, 0)
def _handle_fallback_crc(self, buf: memoryview) -> None:
assert self.temp_crc is not None
assert self.temp_crc_compare is not None
if self.expected_payload_length > len(buf) + self.bytes_read + CHECKSUM_LENGTH:
# The CRC checksum is not in this packet, compute crc over whole buffer
self.temp_crc = checksum.compute_int(buf, self.temp_crc)
elif self.expected_payload_length >= len(buf) + self.bytes_read:
# At least a part of the CRC checksum is in this packet, compute CRC over
# the first (max(0, crc_copy_len)) bytes and add the rest of the bytes
# (max 4) as the checksum from message into temp_crc_compare
crc_copy_len = (
self.expected_payload_length - self.bytes_read - CHECKSUM_LENGTH
)
self.temp_crc = checksum.compute_int(buf[:crc_copy_len], self.temp_crc)
crc_checksum = buf[
self.expected_payload_length
- CHECKSUM_LENGTH
- len(buf)
- self.bytes_read :
]
offset = CHECKSUM_LENGTH - len(buf[-CHECKSUM_LENGTH:])
utils.memcpy(self.temp_crc_compare, offset, crc_checksum, 0)
else:
raise Exception(
f"Buffer (+bytes_read) ({len(buf)}+{self.bytes_read})should not be bigger than payload{self.expected_payload_length}"
)
def _handle_fallback_decryption(self, buf: memoryview) -> None:
assert self.busy_decoder is not None
assert self.temp_tag is not None
if (
self.expected_payload_length
> len(buf) + self.bytes_read + CHECKSUM_LENGTH + TAG_LENGTH
):
# The noise tag is not in this packet, decrypt the whole buffer
self.busy_decoder.decrypt_part(buf)
elif self.expected_payload_length >= len(buf) + self.bytes_read:
# At least a part of the noise tag is in this packet, decrypt
# the first (max(0, dec_len)) bytes and add the rest of the bytes
# as the noise_tag from message into temp_tag
dec_len = (
self.expected_payload_length
- self.bytes_read
- TAG_LENGTH
- CHECKSUM_LENGTH
)
self.busy_decoder.decrypt_part(buf[:dec_len])
noise_tag = buf[
self.expected_payload_length
- CHECKSUM_LENGTH
- TAG_LENGTH
- len(buf)
- self.bytes_read :
]
offset = (
TAG_LENGTH + CHECKSUM_LENGTH - len(buf[-CHECKSUM_LENGTH - TAG_LENGTH :])
)
utils.memcpy(self.temp_tag, offset, noise_tag, 0)
else:
raise Exception("Buffer (+bytes_read) should not be bigger than payload")
def _handle_cont_packet(self, packet: utils.BufferType) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("handle_cont_packet")
if not self.is_cont_packet_expected:
raise ThpError("Continuation packet is not expected, ignoring")
if self.fallback_decrypt:
to_read_len = min(
len(packet) - CONT_HEADER_LENGTH,
self.expected_payload_length - self.bytes_read,
)
buf = memoryview(self.buffer)[:to_read_len]
utils.memcpy(buf, 0, packet, CONT_HEADER_LENGTH)
# CRC CHECK
self._handle_fallback_crc(buf)
# TAG CHECK
self._handle_fallback_decryption(buf)
self.bytes_read += to_read_len
return
try:
buffer = memory_manager.get_existing_read_buffer(self.get_channel_id_int())
except WireBufferError:
self.set_channel_state(ChannelState.INVALIDATED)
pass # TODO handle device busy, channel kaput
self._buffer_packet_data(buffer, packet, CONT_HEADER_LENGTH)
def _buffer_packet_data(
self, payload_buffer: utils.BufferType, packet: utils.BufferType, offset: int
) -> None:
self.bytes_read += utils.memcpy(payload_buffer, self.bytes_read, packet, offset)
def _finish_message(self) -> None:
self.bytes_read = 0
self.expected_payload_length = 0
self.is_cont_packet_expected = False
def _finish_fallback(self) -> None:
self.fallback_decrypt = False
self.busy_decoder = None
def _decrypt_single_packet_payload(
self, payload: utils.BufferType
) -> utils.BufferType:
# crypto.decrypt(b"\x00", b"\x00", payload_buffer, INIT_DATA_OFFSET, len(payload))
return payload
def _prepare_fallback(self) -> None:
# prepare busy decoder
key_receive = self.channel_cache.get(CHANNEL_KEY_RECEIVE)
nonce_receive = self.channel_cache.get_int(CHANNEL_NONCE_RECEIVE)
assert key_receive is not None
assert nonce_receive is not None
self.busy_decoder = crypto.BusyDecoder(key_receive, nonce_receive)
# prepare temp channel values
self.temp_crc = 0
self.temp_crc_compare = bytearray(4)
self.temp_tag = bytearray(16)
# self.bytes_read = INIT_HEADER_LENGTH
def decrypt_buffer(
self, message_length: int, offset: int = INIT_HEADER_LENGTH
) -> None:
buffer = memory_manager.get_existing_read_buffer(self.get_channel_id_int())
# if buffer is WireBufferError:
# pass # TODO handle deviceBUSY
noise_buffer = memoryview(buffer)[
offset : message_length - CHECKSUM_LENGTH - TAG_LENGTH
]
tag = buffer[
message_length
- CHECKSUM_LENGTH
- TAG_LENGTH : message_length
- CHECKSUM_LENGTH
]
if utils.DISABLE_ENCRYPTION:
is_tag_valid = tag == crypto.DUMMY_TAG
else:
key_receive = self.channel_cache.get(CHANNEL_KEY_RECEIVE)
nonce_receive = self.channel_cache.get_int(CHANNEL_NONCE_RECEIVE)
assert key_receive is not None
assert nonce_receive is not None
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("Buffer before decryption: ", get_bytes_as_str(noise_buffer))
is_tag_valid = crypto.dec(noise_buffer, tag, key_receive, nonce_receive)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("Buffer after decryption: ", get_bytes_as_str(noise_buffer))
self.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, nonce_receive + 1)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("Is decrypted tag valid? ", str(is_tag_valid))
self._log("Received tag: ", get_bytes_as_str(tag))
self._log("New nonce_receive: ", str((nonce_receive + 1)))
if not is_tag_valid:
raise ThpDecryptionError()
# WRITE and ENCRYPT
async def write(
self,
msg: protobuf.MessageType,
session_id: int = 0,
force: bool = False,
) -> None:
if __debug__ and utils.EMULATOR:
self._log(f"write message: {msg.MESSAGE_NAME}\n", utils.dump_protobuf(msg))
cid = self.get_channel_id_int()
msg_size = protobuf.encoded_length(msg)
payload_size = SESSION_ID_LENGTH + MESSAGE_TYPE_LENGTH + msg_size
length = payload_size + CHECKSUM_LENGTH + TAG_LENGTH + INIT_HEADER_LENGTH
try:
buffer = memory_manager.get_new_write_buffer(cid, length)
noise_payload_len = memory_manager.encode_into_buffer(
buffer, msg, session_id
)
except WireBufferError:
from trezor.messages import Failure, FailureType
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("Failed to get write buffer, killing channel.")
noise_payload_len = memory_manager.encode_into_buffer(
self.buffer,
Failure(
code=FailureType.FirmwareError,
message="Failed to obtain write buffer.",
),
session_id,
)
self.set_channel_state(ChannelState.INVALIDATED)
task = self._write_and_encrypt(noise_payload_len, force)
if task is not None:
await task
def write_error(self, err_type: int) -> Awaitable[None]:
msg_data = err_type.to_bytes(1, "big")
length = len(msg_data) + CHECKSUM_LENGTH
header = PacketHeader.get_error_header(self.get_channel_id_int(), length)
return write_payload_to_wire_and_add_checksum(self.iface, header, msg_data)
def write_handshake_message(self, ctrl_byte: int, payload: bytes) -> None:
self._prepare_write()
self.write_task_spawn = loop.spawn(
self._write_encrypted_payload_loop(ctrl_byte, payload)
)
def _write_and_encrypt(
self, noise_payload_len: int, force: bool = False
) -> Awaitable[None] | None:
buffer = memory_manager.get_existing_write_buffer(self.get_channel_id_int())
# if buffer is WireBufferError:
# pass # TODO handle deviceBUSY
self._encrypt(buffer, noise_payload_len)
payload_length = noise_payload_len + TAG_LENGTH
if self.write_task_spawn is not None:
self.write_task_spawn.close() # UPS TODO might break something
print("\nCLOSED\n")
self._prepare_write()
if force:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("Writing FORCE message (without async or retransmission).")
return self._write_encrypted_payload_loop(
ENCRYPTED, memoryview(buffer[:payload_length])
)
self.write_task_spawn = loop.spawn(
self._write_encrypted_payload_loop(
ENCRYPTED, memoryview(buffer[:payload_length])
)
)
return None
def _prepare_write(self) -> None:
# TODO add condition that disallows to write when can_send_message is false
ABP.set_sending_allowed(self.channel_cache, False)
async def _write_encrypted_payload_loop(
self, ctrl_byte: int, payload: bytes
) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("write_encrypted_payload_loop")
payload_len = len(payload) + CHECKSUM_LENGTH
sync_bit = ABP.get_send_seq_bit(self.channel_cache)
ctrl_byte = control_byte.add_seq_bit_to_ctrl_byte(ctrl_byte, sync_bit)
header = PacketHeader(ctrl_byte, self.get_channel_id_int(), payload_len)
self.transmission_loop = TransmissionLoop(self, header, payload)
await self.transmission_loop.start()
ABP.set_send_seq_bit_to_opposite(self.channel_cache)
# Let the main loop be restarted and clear loop, if there is no other
# workflow and the state is ENCRYPTED_TRANSPORT
if self._can_clear_loop():
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("clearing loop from channel")
loop.clear()
def _encrypt(self, buffer: utils.BufferType, noise_payload_len: int) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("encrypt")
assert len(buffer) >= noise_payload_len + TAG_LENGTH + CHECKSUM_LENGTH
noise_buffer = memoryview(buffer)[0:noise_payload_len]
if utils.DISABLE_ENCRYPTION:
tag = crypto.DUMMY_TAG
else:
key_send = self.channel_cache.get(CHANNEL_KEY_SEND)
nonce_send = self.channel_cache.get_int(CHANNEL_NONCE_SEND)
assert key_send is not None
assert nonce_send is not None
tag = crypto.enc(noise_buffer, key_send, nonce_send)
self.channel_cache.set_int(CHANNEL_NONCE_SEND, nonce_send + 1)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
self._log("New nonce_send: ", str((nonce_send + 1)))
buffer[noise_payload_len : noise_payload_len + TAG_LENGTH] = tag
def _can_clear_loop(self) -> bool:
return (
not workflow.tasks
) and self.get_channel_state() is ChannelState.ENCRYPTED_TRANSPORT
if __debug__:
def _log(self, text_1: str, text_2: str = "") -> None:
log.debug(
__name__,
"(cid: %s) %s%s",
get_bytes_as_str(self.channel_id),
text_1,
text_2,
)

View File

@ -0,0 +1,30 @@
from typing import TYPE_CHECKING
from storage import cache_thp
from . import ChannelState, interface_manager
from .channel import Channel
if TYPE_CHECKING:
from trezorio import WireInterface
def create_new_channel(iface: WireInterface) -> Channel:
"""
Creates a new channel for the interface `iface`.
"""
channel_cache = cache_thp.get_new_channel(interface_manager.encode_iface(iface))
channel = Channel(channel_cache)
channel.set_channel_state(ChannelState.TH1)
return channel
def load_cached_channels() -> dict[int, Channel]:
"""
Returns all allocated channels from cache.
"""
channels: dict[int, Channel] = {}
cached_channels = cache_thp.get_all_allocated_channels()
for channel in cached_channels:
channels[int.from_bytes(channel.channel_id, "big")] = Channel(channel)
return channels

View File

@ -0,0 +1,33 @@
from micropython import const
from trezor import utils
from trezor.crypto import crc
CHECKSUM_LENGTH = const(4)
def compute(data: bytes | utils.BufferType, crc_chain: int = 0) -> bytes:
"""
Returns a CRC-32 checksum of the provided `data`. Allows for for chaining
computations over multiple data segments using `crc_chain` (optional).
"""
return crc.crc32(data, crc_chain).to_bytes(CHECKSUM_LENGTH, "big")
def compute_int(data: bytes | utils.BufferType, crc_chain: int = 0) -> int:
"""
Returns a CRC-32 checksum of the provided `data`. Allows for for chaining
computations over multiple data segments using `crc_chain` (optional).
Returns checksum in the form of `int`.
"""
return crc.crc32(data, crc_chain)
def is_valid(checksum: bytes | utils.BufferType, data: bytes) -> bool:
"""
Checks whether the CRC-32 checksum of the `data` is the same
as the checksum provided in `checksum`.
"""
data_checksum = compute(data)
return checksum == data_checksum

View File

@ -0,0 +1,50 @@
from micropython import const
from . import (
ACK_MESSAGE,
CONTINUATION_PACKET,
ENCRYPTED,
HANDSHAKE_COMP_REQ,
HANDSHAKE_INIT_REQ,
ThpError,
)
_CONTINUATION_PACKET_MASK = const(0x80)
_ACK_MASK = const(0xF7)
_DATA_MASK = const(0xE7)
def add_seq_bit_to_ctrl_byte(ctrl_byte: int, seq_bit: int) -> int:
if seq_bit == 0:
return ctrl_byte & 0xEF
if seq_bit == 1:
return ctrl_byte | 0x10
raise ThpError("Unexpected sequence bit")
def add_ack_bit_to_ctrl_byte(ctrl_byte: int, ack_bit: int) -> int:
if ack_bit == 0:
return ctrl_byte & 0xF7
if ack_bit == 1:
return ctrl_byte | 0x08
raise ThpError("Unexpected acknowledgement bit")
def is_ack(ctrl_byte: int) -> bool:
return ctrl_byte & _ACK_MASK == ACK_MESSAGE
def is_continuation(ctrl_byte: int) -> bool:
return ctrl_byte & _CONTINUATION_PACKET_MASK == CONTINUATION_PACKET
def is_encrypted_transport(ctrl_byte: int) -> bool:
return ctrl_byte & _DATA_MASK == ENCRYPTED
def is_handshake_init_req(ctrl_byte: int) -> bool:
return ctrl_byte & _DATA_MASK == HANDSHAKE_INIT_REQ
def is_handshake_comp_req(ctrl_byte: int) -> bool:
return ctrl_byte & _DATA_MASK == HANDSHAKE_COMP_REQ

View File

@ -0,0 +1,37 @@
from trezor.crypto import elligator2, random
from trezor.crypto.curve import curve25519
from trezor.crypto.hashlib import sha512
_PREFIX = b"\x08\x43\x50\x61\x63\x65\x32\x35\x35\x06"
_PADDING = b"\x6f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20"
class Cpace:
"""
CPace, a balanced composable PAKE: https://datatracker.ietf.org/doc/draft-irtf-cfrg-cpace/
"""
def __init__(self, handshake_hash: bytes) -> None:
self.handshake_hash: bytes = handshake_hash
self.shared_secret: bytes
self.trezor_private_key: bytes
self.trezor_public_key: bytes
def generate_keys_and_secret(self, code_code_entry: bytes) -> None:
"""
Generate ephemeral key pair and a shared secret using Elligator2 with X25519.
"""
sha_ctx = sha512(_PREFIX)
sha_ctx.update(code_code_entry)
sha_ctx.update(_PADDING)
sha_ctx.update(self.handshake_hash)
sha_ctx.update(b"\x00")
pregenerator = sha_ctx.digest()[:32]
generator = elligator2.map_to_curve25519(pregenerator)
self.trezor_private_key = random.bytes(32)
self.trezor_public_key = curve25519.multiply(self.trezor_private_key, generator)
def compute_shared_secret(self, host_public_key: bytes) -> None:
self.shared_secret = curve25519.multiply(
self.trezor_private_key, host_public_key
)

View File

@ -0,0 +1,220 @@
from micropython import const
from trezorcrypto import aesgcm, bip32, curve25519, hmac
from storage import device
from trezor import log, utils
from trezor.crypto.hashlib import sha256
from trezor.wire.thp import ThpDecryptionError
# The HARDENED flag is taken from apps.common.paths
# It is not imported to save on resources
HARDENED = const(0x8000_0000)
PUBKEY_LENGTH = const(32)
if utils.DISABLE_ENCRYPTION:
DUMMY_TAG = b"\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xB0\xB1\xB2\xB3\xB4\xB5"
if __debug__:
from trezor.utils import get_bytes_as_str
def enc(
buffer: utils.BufferType, key: bytes, nonce: int, auth_data: bytes = b""
) -> bytes:
"""
Encrypts the provided `buffer` with AES-GCM (in place).
Returns a 16-byte long encryption tag.
"""
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "enc (key: %s, nonce: %d)", get_bytes_as_str(key), nonce)
iv = _get_iv_from_nonce(nonce)
aes_ctx = aesgcm(key, iv)
aes_ctx.auth(auth_data)
aes_ctx.encrypt_in_place(buffer)
return aes_ctx.finish()
def dec(
buffer: utils.BufferType, tag: bytes, key: bytes, nonce: int, auth_data: bytes = b""
) -> bool:
"""
Decrypts the provided buffer (in place). Returns `True` if the provided authentication `tag` is the same as
the tag computed in decryption, otherwise it returns `False`.
"""
iv = _get_iv_from_nonce(nonce)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "dec (key: %s, nonce: %d)", get_bytes_as_str(key), nonce)
aes_ctx = aesgcm(key, iv)
aes_ctx.auth(auth_data)
aes_ctx.decrypt_in_place(buffer)
computed_tag = aes_ctx.finish()
return computed_tag == tag
class BusyDecoder:
def __init__(self, key: bytes, nonce: int, auth_data: bytes = b"") -> None:
iv = _get_iv_from_nonce(nonce)
self.aes_ctx = aesgcm(key, iv)
self.aes_ctx.auth(auth_data)
def decrypt_part(self, part: utils.BufferType) -> None:
self.aes_ctx.decrypt_in_place(part)
def finish_and_check_tag(self, tag: bytes) -> bool:
computed_tag = self.aes_ctx.finish()
return computed_tag == tag
PROTOCOL_NAME = b"Noise_XX_25519_AESGCM_SHA256\x00\x00\x00\x00"
IV_1 = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
IV_2 = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
class Handshake:
"""
`Handshake` holds (temporary) values and keys that are used during the creation of an encrypted channel.
The following values should be saved for future use before disposing of this object:
- `h` - handshake hash, can be used to bind other values to the channel
- `key_receive` - key for decrypting incoming communication
- `key_send` - key for encrypting outgoing communication
"""
def __init__(self) -> None:
self.trezor_ephemeral_privkey: bytes
self.ck: bytes
self.k: bytes
self.h: bytes
self.key_receive: bytes
self.key_send: bytes
def handle_th1_crypto(
self,
device_properties: bytes,
host_ephemeral_pubkey: bytes,
) -> tuple[bytes, bytes, bytes]:
trezor_static_privkey, trezor_static_pubkey = _derive_static_key_pair()
self.trezor_ephemeral_privkey = curve25519.generate_secret()
trezor_ephemeral_pubkey = curve25519.publickey(self.trezor_ephemeral_privkey)
self.h = _hash_of_two(PROTOCOL_NAME, device_properties)
self.h = _hash_of_two(self.h, host_ephemeral_pubkey)
self.h = _hash_of_two(self.h, trezor_ephemeral_pubkey)
point = curve25519.multiply(
self.trezor_ephemeral_privkey, host_ephemeral_pubkey
)
self.ck, self.k = _hkdf(PROTOCOL_NAME, point)
mask = _hash_of_two(trezor_static_pubkey, trezor_ephemeral_pubkey)
trezor_masked_static_pubkey = curve25519.multiply(mask, trezor_static_pubkey)
aes_ctx = aesgcm(self.k, IV_1)
encrypted_trezor_static_pubkey = aes_ctx.encrypt(trezor_masked_static_pubkey)
if __debug__:
log.debug(
__name__, "th1 - enc (key: %s, nonce: %d)", get_bytes_as_str(self.k), 0
)
aes_ctx.auth(self.h)
tag_to_encrypted_key = aes_ctx.finish()
encrypted_trezor_static_pubkey = (
encrypted_trezor_static_pubkey + tag_to_encrypted_key
)
self.h = _hash_of_two(self.h, encrypted_trezor_static_pubkey)
point = curve25519.multiply(trezor_static_privkey, host_ephemeral_pubkey)
self.ck, self.k = _hkdf(self.ck, curve25519.multiply(mask, point))
aes_ctx = aesgcm(self.k, IV_1)
aes_ctx.auth(self.h)
tag = aes_ctx.finish()
self.h = _hash_of_two(self.h, tag)
return (trezor_ephemeral_pubkey, encrypted_trezor_static_pubkey, tag)
def handle_th2_crypto(
self,
encrypted_host_static_pubkey: utils.BufferType,
encrypted_payload: utils.BufferType,
) -> None:
aes_ctx = aesgcm(self.k, IV_2)
# The new value of hash `h` MUST be computed before the `encrypted_host_static_pubkey` is decrypted.
# However, decryption of `encrypted_host_static_pubkey` MUST use the previous value of `h` for
# authentication of the gcm tag.
aes_ctx.auth(self.h) # Authenticate with the previous value of `h`
self.h = _hash_of_two(self.h, encrypted_host_static_pubkey) # Compute new value
aes_ctx.decrypt_in_place(
memoryview(encrypted_host_static_pubkey)[:PUBKEY_LENGTH]
)
if __debug__:
log.debug(
__name__, "th2 - dec (key: %s, nonce: %d)", get_bytes_as_str(self.k), 1
)
host_static_pubkey = memoryview(encrypted_host_static_pubkey)[:PUBKEY_LENGTH]
tag = aes_ctx.finish()
if tag != encrypted_host_static_pubkey[-16:]:
raise ThpDecryptionError()
self.ck, self.k = _hkdf(
self.ck,
curve25519.multiply(self.trezor_ephemeral_privkey, host_static_pubkey),
)
aes_ctx = aesgcm(self.k, IV_1)
aes_ctx.auth(self.h)
aes_ctx.decrypt_in_place(memoryview(encrypted_payload)[:-16])
if __debug__:
log.debug(
__name__, "th2 - dec (key: %s, nonce: %d)", get_bytes_as_str(self.k), 0
)
tag = aes_ctx.finish()
if tag != encrypted_payload[-16:]:
raise ThpDecryptionError()
self.h = _hash_of_two(self.h, memoryview(encrypted_payload)[:-16])
self.key_receive, self.key_send = _hkdf(self.ck, b"")
if __debug__:
log.debug(
__name__,
"(key_receive: %s, key_send: %s)",
get_bytes_as_str(self.key_receive),
get_bytes_as_str(self.key_send),
)
def get_handshake_completion_response(self, trezor_state: bytes) -> bytes:
aes_ctx = aesgcm(self.key_send, IV_1)
encrypted_trezor_state = aes_ctx.encrypt(trezor_state)
tag = aes_ctx.finish()
return encrypted_trezor_state + tag
def _derive_static_key_pair() -> tuple[bytes, bytes]:
node_int = HARDENED | int.from_bytes(b"\x00THP", "big")
node = bip32.from_seed(device.get_device_secret(), "curve25519")
node.derive(node_int)
trezor_static_privkey = node.private_key()
trezor_static_pubkey = node.public_key()[1:33]
# Note: the first byte (\x01) of the public key is removed, as it
# only indicates the type of the elliptic curve used
return trezor_static_privkey, trezor_static_pubkey
def get_trezor_static_pubkey() -> bytes:
_, pubkey = _derive_static_key_pair()
return pubkey
def _hkdf(chaining_key: bytes, input: bytes) -> tuple[bytes, bytes]:
temp_key = hmac(hmac.SHA256, chaining_key, input).digest()
output_1 = hmac(hmac.SHA256, temp_key, b"\x01").digest()
ctx_output_2 = hmac(hmac.SHA256, temp_key, output_1)
ctx_output_2.update(b"\x02")
output_2 = ctx_output_2.digest()
return (output_1, output_2)
def _hash_of_two(part_1: bytes, part_2: bytes) -> bytes:
ctx = sha256(part_1)
ctx.update(part_2)
return ctx.digest()
def _get_iv_from_nonce(nonce: int) -> bytes:
utils.ensure(nonce <= 0xFFFFFFFFFFFFFFFF, "Nonce overflow, terminate the channel")
return bytes(4) + nonce.to_bytes(8, "big")

View File

@ -0,0 +1,28 @@
from typing import TYPE_CHECKING
import usb
_WIRE_INTERFACE_USB = b"\x01"
# TODO _WIRE_INTERFACE_BLE = b"\x02"
if TYPE_CHECKING:
from trezorio import WireInterface
def decode_iface(cached_iface: bytes) -> WireInterface:
"""Decode the cached wire interface."""
if cached_iface == _WIRE_INTERFACE_USB:
iface = usb.iface_wire
if iface is None:
raise RuntimeError("There is no valid USB WireInterface")
return iface
# TODO implement bluetooth interface
raise Exception("Unknown WireInterface")
def encode_iface(iface: WireInterface) -> bytes:
"""Encode wire interface into bytes."""
if iface is usb.iface_wire:
return _WIRE_INTERFACE_USB
# TODO implement bluetooth interface
raise Exception("Unknown WireInterface")

View File

@ -0,0 +1,180 @@
import utime
from micropython import const
from storage.cache_thp import SESSION_ID_LENGTH
from trezor import protobuf, utils
from trezor.wire.errors import WireBufferError
from trezor.wire.message_handler import get_msg_type
from . import ThpError
from .writer import MAX_PAYLOAD_LEN, MESSAGE_TYPE_LENGTH
_PROTOBUF_BUFFER_SIZE = 8192
READ_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
WRITE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
LOCK_TIMEOUT = 200 # miliseconds
lock_owner_cid: int | None = None
lock_time: int = 0
READ_BUFFER_SLICE: memoryview | None = None
WRITE_BUFFER_SLICE: memoryview | None = None
# Buffer types
_READ: int = const(0)
_WRITE: int = const(1)
#
# Access to buffer slices
def get_new_read_buffer(channel_id: int, length: int) -> memoryview:
return _get_new_buffer(_READ, channel_id, length)
def get_new_write_buffer(channel_id: int, length: int) -> memoryview:
return _get_new_buffer(_WRITE, channel_id, length)
def get_existing_read_buffer(channel_id: int) -> memoryview:
return _get_existing_buffer(_READ, channel_id)
def get_existing_write_buffer(channel_id: int) -> memoryview:
return _get_existing_buffer(_WRITE, channel_id)
def _get_new_buffer(buffer_type: int, channel_id: int, length: int) -> memoryview:
if is_locked():
if not is_owner(channel_id):
raise WireBufferError
update_lock_time()
else:
update_lock(channel_id)
if buffer_type == _READ:
global READ_BUFFER
buffer = READ_BUFFER
elif buffer_type == _WRITE:
global WRITE_BUFFER
buffer = WRITE_BUFFER
else:
raise Exception("Invalid buffer_type")
if length > MAX_PAYLOAD_LEN or length > len(buffer):
raise ThpError("Message is too large") # TODO reword
if buffer_type == _READ:
global READ_BUFFER_SLICE
READ_BUFFER_SLICE = memoryview(READ_BUFFER)[:length]
return READ_BUFFER_SLICE
if buffer_type == _WRITE:
global WRITE_BUFFER_SLICE
WRITE_BUFFER_SLICE = memoryview(WRITE_BUFFER)[:length]
return WRITE_BUFFER_SLICE
raise Exception("Invalid buffer_type")
def _get_existing_buffer(buffer_type: int, channel_id: int) -> memoryview:
if not is_owner(channel_id):
raise WireBufferError
update_lock_time()
if buffer_type == _READ:
global READ_BUFFER_SLICE
if READ_BUFFER_SLICE is None:
raise WireBufferError
return READ_BUFFER_SLICE
if buffer_type == _WRITE:
global WRITE_BUFFER_SLICE
if WRITE_BUFFER_SLICE is None:
raise WireBufferError
return WRITE_BUFFER_SLICE
raise Exception("Invalid buffer_type")
#
# Buffer locking
def is_locked() -> bool:
global lock_owner_cid
global lock_time
time_diff = utime.ticks_diff(utime.ticks_ms(), lock_time)
return lock_owner_cid is not None and time_diff < LOCK_TIMEOUT
def is_owner(channel_id: int) -> bool:
global lock_owner_cid
return lock_owner_cid is not None and lock_owner_cid == channel_id
def update_lock(channel_id: int) -> None:
set_owner(channel_id)
update_lock_time()
def set_owner(channel_id: int) -> None:
global lock_owner_cid
lock_owner_cid = channel_id
def update_lock_time() -> None:
global lock_time
lock_time = utime.ticks_ms()
#
# Helper for encoding messages into buffer
def encode_into_buffer(
buffer: utils.BufferType, msg: protobuf.MessageType, session_id: int
) -> int:
"""Encode protobuf message `msg` into the `buffer`, including session id
an messages's wire type. Will fail if provided message has no wire type."""
# cannot write message without wire type
msg_type = msg.MESSAGE_WIRE_TYPE
if msg_type is None:
msg_type = get_msg_type(msg.MESSAGE_NAME)
if msg_type is None:
raise Exception("Message has no wire type.")
msg_size = protobuf.encoded_length(msg)
payload_size = SESSION_ID_LENGTH + MESSAGE_TYPE_LENGTH + msg_size
_encode_session_into_buffer(memoryview(buffer), session_id)
_encode_message_type_into_buffer(memoryview(buffer), msg_type, SESSION_ID_LENGTH)
_encode_message_into_buffer(
memoryview(buffer), msg, SESSION_ID_LENGTH + MESSAGE_TYPE_LENGTH
)
return payload_size
def _encode_session_into_buffer(
buffer: memoryview, session_id: int, buffer_offset: int = 0
) -> None:
session_id_bytes = int.to_bytes(session_id, SESSION_ID_LENGTH, "big")
utils.memcpy(buffer, buffer_offset, session_id_bytes, 0)
def _encode_message_type_into_buffer(
buffer: memoryview, message_type: int, offset: int = 0
) -> None:
msg_type_bytes = int.to_bytes(message_type, MESSAGE_TYPE_LENGTH, "big")
utils.memcpy(buffer, offset, msg_type_bytes, 0)
def _encode_message_into_buffer(
buffer: memoryview, message: protobuf.MessageType, buffer_offset: int = 0
) -> None:
protobuf.encode(memoryview(buffer[buffer_offset:]), message)

View File

@ -0,0 +1,329 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
import trezorui_api
from trezor import loop, protobuf, workflow
from trezor.enums import ButtonRequestType
from trezor.wire import context, message_handler, protocol_common
from trezor.wire.context import UnexpectedMessageException
from trezor.wire.errors import ActionCancelled, SilentError
from trezor.wire.protocol_common import Context, Message
from trezor.wire.thp import ChannelState, get_enabled_pairing_methods
if TYPE_CHECKING:
from typing import Awaitable, Container
from trezor.enums import ThpPairingMethod
from trezorui_api import UiResult
from .channel import Channel
from .cpace import Cpace
pass
if __debug__:
from trezor import log
class PairingContext(Context):
def __init__(self, channel_ctx: Channel) -> None:
super().__init__(channel_ctx.iface, channel_ctx.channel_id)
self.channel_ctx: Channel = channel_ctx
self.incoming_message = loop.mailbox()
self.nfc_secret: bytes | None = None
self.qr_code_secret: bytes | None = None
self.code_entry_secret: bytes | None = None
self.selected_method: ThpPairingMethod
self.code_code_entry: int | None = None
self.code_qr_code: bytes | None = None
self.code_nfc: bytes | None = None
# The 2 following attributes are important for NFC pairing
self.nfc_secret_host: bytes | None = None
self.handshake_hash_host: bytes | None = None
self.cpace: Cpace
self.host_name: str | None
async def handle(self) -> None:
next_message: Message | None = None
while True:
try:
if next_message is None:
# If the previous run did not keep an unprocessed message for us,
# wait for a new one.
try:
message: Message = await self.incoming_message
except protocol_common.WireError as e:
if __debug__:
log.exception(__name__, e)
await self.write(message_handler.failure(e))
continue
else:
# Process the message from previous run.
message = next_message
next_message = None
try:
next_message = await handle_message(self, message)
except Exception as exc:
# Log and ignore. The context handler can only exit explicitly in the
# following finally block.
if __debug__:
log.exception(__name__, exc)
finally:
if next_message is None:
# Shut down the loop if there is no next message waiting.
return # pylint: disable=lost-exception
except Exception as exc:
# Log and try again. The context handler can only exit explicitly via
# finally block above
if __debug__:
log.exception(__name__, exc)
async def read(
self,
expected_types: Container[int],
expected_type: type[protobuf.MessageType] | None = None,
) -> protobuf.MessageType:
if __debug__:
exp_type: str = str(expected_type)
if expected_type is not None:
exp_type = expected_type.MESSAGE_NAME
log.debug(
__name__,
"Read - with expected types %s and expected type %s",
str(expected_types),
exp_type,
)
message: Message = await self.incoming_message
if message.type not in expected_types:
raise UnexpectedMessageException(message)
if expected_type is None:
name = message_handler.get_msg_name(message.type)
if name is None:
expected_type = protobuf.type_for_wire(message.type)
else:
expected_type = protobuf.type_for_name(name)
return message_handler.wrap_protobuf_load(message.data, expected_type)
async def write(self, msg: protobuf.MessageType) -> None:
return await self.channel_ctx.write(msg)
def write_force(self, msg: protobuf.MessageType) -> Awaitable[None]:
return self.channel_ctx.write(msg, force=True)
async def call(
self, msg: protobuf.MessageType, expected_type: type[protobuf.MessageType]
) -> protobuf.MessageType:
expected_wire_type = expected_type.MESSAGE_WIRE_TYPE
if expected_wire_type is None:
expected_wire_type = message_handler.get_msg_type(
expected_type.MESSAGE_NAME
)
assert expected_wire_type is not None
await self.write(msg)
del msg
return await self.read((expected_wire_type,), expected_type)
async def call_any(
self, msg: protobuf.MessageType, *expected_types: int
) -> protobuf.MessageType:
await self.write(msg)
del msg
return await self.read(expected_types)
def set_selected_method(self, selected_method: ThpPairingMethod) -> None:
if selected_method not in get_enabled_pairing_methods(self.iface):
raise Exception("Not allowed to set this method")
self.selected_method = selected_method
async def show_pairing_dialogue(self) -> None:
from trezor.messages import ThpPairingRequestApproved
from trezor.ui.layouts.common import interact
result = await interact(
trezorui_api.confirm_action(
title="Pairing dialogue",
action="Do you want to start pairing?",
description="Choose wisely!",
),
br_name="pairing_request",
br_code=ButtonRequestType.Other,
)
if result == trezorui_api.CONFIRMED:
await self.write(ThpPairingRequestApproved())
async def show_connection_dialogue(self) -> None:
from trezor.ui.layouts.common import interact
await interact(
trezorui_api.confirm_action(
title="Connection dialogue",
action="Do you want previously connected device to connect?",
description="Choose wisely! (or not)",
),
br_name="connection_request",
br_code=ButtonRequestType.Other,
)
async def show_pairing_method_screen(
self, selected_method: ThpPairingMethod | None = None
) -> UiResult:
from trezor.enums import ThpPairingMethod
if selected_method is None:
selected_method = self.selected_method
if selected_method is ThpPairingMethod.CodeEntry:
return await self._show_code_entry_screen()
elif selected_method is ThpPairingMethod.NFC:
return await self._show_nfc_screen()
elif selected_method is ThpPairingMethod.QrCode:
return await self._show_qr_code_screen()
else:
raise Exception("Unknown pairing method")
async def _show_code_entry_screen(self) -> UiResult:
from trezor.ui.layouts.common import interact
return await interact(
trezorui_api.show_simple(
title="Copy the following",
text=self._get_code_code_entry_str(),
),
br_name="pairing_code_entry",
br_code=ButtonRequestType.Other,
)
async def _show_nfc_screen(self) -> UiResult:
from trezor.ui.layouts.common import interact
return await interact(
trezorui_api.show_simple(
title="NFC Pairing",
text="Move your device close to Trezor",
),
br_name="pairing_nfc",
br_code=ButtonRequestType.Other,
)
async def _show_qr_code_screen(self) -> UiResult:
from trezor.ui.layouts.common import interact
return await interact(
trezorui_api.show_address_details( # noqa
qr_title="Scan QR code to pair",
address=self._get_code_qr_code_str(),
case_sensitive=True,
details_title="",
account="",
path="",
xpubs=[],
),
br_name="pairing_qr_code",
br_code=ButtonRequestType.Other,
)
def _get_code_code_entry_str(self) -> str:
if self.code_code_entry is not None:
code_str = f"{self.code_code_entry:06}"
if __debug__:
log.debug(__name__, "code_code_entry: %s", code_str)
return code_str[:3] + " " + code_str[3:]
raise Exception("Code entry string is not available")
def _get_code_qr_code_str(self) -> str:
if self.code_qr_code is not None:
code_str = (hexlify(self.code_qr_code)).decode("utf-8")
if __debug__:
log.debug(__name__, "code_qr_code_hexlified: %s", code_str)
return code_str
raise Exception("QR code string is not available")
async def handle_message(
pairing_ctx: PairingContext,
msg: protocol_common.Message,
) -> protocol_common.Message | None:
res_msg: protobuf.MessageType | None = None
from apps.thp.pairing import handle_credential_phase, handle_pairing_request
if msg.type in workflow.ALLOW_WHILE_LOCKED:
workflow.autolock_interrupts_workflow = False
# Here we make sure we always respond with a Failure response
# in case of any errors.
try:
# Find a protobuf.MessageType subclass that describes this
# message. Raises if the type is not found.
name = message_handler.get_msg_name(msg.type)
if name is None:
req_type = protobuf.type_for_wire(msg.type)
else:
req_type = protobuf.type_for_name(name)
# Try to decode the message according to schema from
# `req_type`. Raises if the message is malformed.
req_msg = message_handler.wrap_protobuf_load(msg.data, req_type)
# Create the handler task.
if pairing_ctx.channel_ctx.get_channel_state() == ChannelState.TC1:
task = handle_credential_phase(pairing_ctx, req_msg)
else:
task = handle_pairing_request(pairing_ctx, req_msg)
# Run the workflow task. Workflow can do more on-the-wire
# communication inside, but it should eventually return a
# response message, or raise an exception (a rather common
# thing to do). Exceptions are handled in the code below.
res_msg = await workflow.spawn(context.with_context(pairing_ctx, task))
except UnexpectedMessageException as exc:
# Workflow was trying to read a message from the wire, and
# something unexpected came in. See Context.read() for
# example, which expects some particular message and raises
# UnexpectedMessage if another one comes in.
# In order not to lose the message, we return it to the caller.
# TODO:
# We might handle only the few common cases here, like
# Initialize and Cancel.
return exc.msg
except SilentError as exc:
if __debug__:
log.error(__name__, "SilentError: %s", exc.message)
except BaseException as exc:
# Either:
# - the message had a type that has a registered handler, but does not have
# a protobuf class
# - the message was not valid protobuf
# - workflow raised some kind of an exception while running
# - something canceled the workflow from the outside
if __debug__:
if isinstance(exc, ActionCancelled):
log.debug(__name__, "cancelled: %s", exc.message)
elif isinstance(exc, loop.TaskClosed):
log.debug(__name__, "cancelled: loop task was closed")
else:
log.exception(__name__, exc)
res_msg = message_handler.failure(exc)
if res_msg is not None:
# perform the write outside the big try-except block, so that usb write
# problem bubbles up
await pairing_ctx.write(res_msg)
return None

View File

@ -0,0 +1,462 @@
import ustruct
from typing import TYPE_CHECKING
from storage.cache_common import (
CHANNEL_HANDSHAKE_HASH,
CHANNEL_KEY_RECEIVE,
CHANNEL_KEY_SEND,
CHANNEL_NONCE_RECEIVE,
CHANNEL_NONCE_SEND,
)
from storage.cache_thp import (
KEY_LENGTH,
SESSION_ID_LENGTH,
TAG_LENGTH,
update_channel_last_used,
update_session_last_used,
)
from trezor import config, log, loop, protobuf, utils
from trezor.enums import FailureType
from trezor.messages import Failure
from trezor.wire.thp import memory_manager
from .. import message_handler
from ..errors import DataError
from ..protocol_common import Message
from . import (
ACK_MESSAGE,
HANDSHAKE_COMP_RES,
HANDSHAKE_INIT_RES,
ChannelState,
PacketHeader,
SessionState,
ThpDecryptionError,
ThpDeviceLockedError,
ThpError,
ThpErrorType,
ThpInvalidDataError,
ThpUnallocatedSessionError,
)
from . import alternating_bit_protocol as ABP
from . import checksum, control_byte, get_encoded_device_properties, session_manager
from .checksum import CHECKSUM_LENGTH
from .crypto import PUBKEY_LENGTH, Handshake
from .session_context import SeedlessSessionContext
from .writer import (
INIT_HEADER_LENGTH,
MESSAGE_TYPE_LENGTH,
write_payload_to_wire_and_add_checksum,
)
if TYPE_CHECKING:
from typing import Awaitable
from trezor.messages import ThpHandshakeCompletionReqNoisePayload
from .channel import Channel
if __debug__:
from trezor.utils import get_bytes_as_str
_TREZOR_STATE_UNPAIRED = b"\x00"
_TREZOR_STATE_PAIRED = b"\x01"
_TREZOR_STATE_PAIRED_AUTOCONNECT = b"\x02"
async def handle_received_message(
ctx: Channel, message_buffer: utils.BufferType
) -> None:
"""Handle a message received from the channel."""
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "handle_received_message")
if utils.ALLOW_DEBUG_MESSAGES: # TODO remove after performance tests are done
try:
import micropython
print("micropython.mem_info() from received_message_handler.py")
micropython.mem_info()
print("Allocation count:", micropython.alloc_count()) # type: ignore ["alloc_count" is not a known attribute of module "micropython"]
except AttributeError:
print(
"To show allocation count, create the build with TREZOR_MEMPERF=1"
)
ctrl_byte, _, payload_length = ustruct.unpack(">BHH", message_buffer)
message_length = payload_length + INIT_HEADER_LENGTH
_check_checksum(message_length, message_buffer)
# Synchronization process
seq_bit = (ctrl_byte & 0x10) >> 4
ack_bit = (ctrl_byte & 0x08) >> 3
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"handle_completed_message - seq bit of message: %d, ack bit of message: %d",
seq_bit,
ack_bit,
)
# 0: Update "last-time used"
update_channel_last_used(ctx.channel_id)
# 1: Handle ACKs
if control_byte.is_ack(ctrl_byte):
await _handle_ack(ctx, ack_bit)
return
if _should_have_ctrl_byte_encrypted_transport(
ctx
) and not control_byte.is_encrypted_transport(ctrl_byte):
raise ThpError("Message is not encrypted. Ignoring")
# 2: Handle message with unexpected sequential bit
if seq_bit != ABP.get_expected_receive_seq_bit(ctx.channel_cache):
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "Received message with an unexpected sequential bit")
await _send_ack(ctx, ack_bit=seq_bit)
raise ThpError("Received message with an unexpected sequential bit")
# 3: Send ACK in response
await _send_ack(ctx, ack_bit=seq_bit)
ABP.set_expected_receive_seq_bit(ctx.channel_cache, 1 - seq_bit)
try:
await _handle_message_to_app_or_channel(
ctx, payload_length, message_length, ctrl_byte
)
except ThpUnallocatedSessionError as e:
error_message = Failure(code=FailureType.ThpUnallocatedSession)
await ctx.write(error_message, e.session_id)
except ThpDecryptionError:
await ctx.write_error(ThpErrorType.DECRYPTION_FAILED)
ctx.clear()
except ThpInvalidDataError:
await ctx.write_error(ThpErrorType.INVALID_DATA)
ctx.clear()
except ThpDeviceLockedError:
await ctx.write_error(ThpErrorType.DEVICE_LOCKED)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "handle_received_message - end")
def _send_ack(ctx: Channel, ack_bit: int) -> Awaitable[None]:
ctrl_byte = control_byte.add_ack_bit_to_ctrl_byte(ACK_MESSAGE, ack_bit)
header = PacketHeader(ctrl_byte, ctx.get_channel_id_int(), CHECKSUM_LENGTH)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"Writing ACK message to a channel with id: %d, ack_bit: %d",
ctx.get_channel_id_int(),
ack_bit,
)
return write_payload_to_wire_and_add_checksum(ctx.iface, header, b"")
def _check_checksum(message_length: int, message_buffer: utils.BufferType) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "check_checksum")
if not checksum.is_valid(
checksum=message_buffer[message_length - CHECKSUM_LENGTH : message_length],
data=memoryview(message_buffer)[: message_length - CHECKSUM_LENGTH],
):
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "Invalid checksum, ignoring message.")
raise ThpError("Invalid checksum, ignoring message.")
async def _handle_ack(ctx: Channel, ack_bit: int) -> None:
if not ABP.is_ack_valid(ctx.channel_cache, ack_bit):
return
# ACK is expected and it has correct sync bit
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "Received ACK message with correct ack bit")
if ctx.transmission_loop is not None:
ctx.transmission_loop.stop_immediately()
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "Stopped transmission loop")
ABP.set_sending_allowed(ctx.channel_cache, True)
if ctx.write_task_spawn is not None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, 'Control to "write_encrypted_payload_loop" task')
await ctx.write_task_spawn
# Note that no the write_task_spawn could result in loop.clear(),
# which will result in termination of this function - any code after
# this await might not be executed
def _handle_message_to_app_or_channel(
ctx: Channel,
payload_length: int,
message_length: int,
ctrl_byte: int,
) -> Awaitable[None]:
state = ctx.get_channel_state()
if state is ChannelState.ENCRYPTED_TRANSPORT:
return _handle_state_ENCRYPTED_TRANSPORT(ctx, message_length)
if state is ChannelState.TH1:
return _handle_state_TH1(ctx, payload_length, message_length, ctrl_byte)
if state is ChannelState.TH2:
return _handle_state_TH2(ctx, message_length, ctrl_byte)
if _is_channel_state_pairing(state):
return _handle_pairing(ctx, message_length)
raise ThpError("Unimplemented channel state")
async def _handle_state_TH1(
ctx: Channel,
payload_length: int,
message_length: int,
ctrl_byte: int,
) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "handle_state_TH1")
if not control_byte.is_handshake_init_req(ctrl_byte):
raise ThpError("Message received is not a handshake init request!")
if not payload_length == PUBKEY_LENGTH + CHECKSUM_LENGTH:
raise ThpError("Message received is not a valid handshake init request!")
if not config.is_unlocked():
raise ThpDeviceLockedError
ctx.handshake = Handshake()
buffer = memory_manager.get_existing_read_buffer(ctx.get_channel_id_int())
# if buffer is BufferError:
# pass # TODO buffer is gone :/
host_ephemeral_pubkey = bytearray(
buffer[INIT_HEADER_LENGTH : message_length - CHECKSUM_LENGTH]
)
trezor_ephemeral_pubkey, encrypted_trezor_static_pubkey, tag = (
ctx.handshake.handle_th1_crypto(
get_encoded_device_properties(ctx.iface), host_ephemeral_pubkey
)
)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"trezor ephemeral pubkey: %s",
get_bytes_as_str(trezor_ephemeral_pubkey),
)
log.debug(
__name__,
"encrypted trezor masked static pubkey: %s",
get_bytes_as_str(encrypted_trezor_static_pubkey),
)
log.debug(__name__, "tag: %s", get_bytes_as_str(tag))
payload = trezor_ephemeral_pubkey + encrypted_trezor_static_pubkey + tag
# send handshake init response message
ctx.write_handshake_message(HANDSHAKE_INIT_RES, payload)
ctx.set_channel_state(ChannelState.TH2)
return
async def _handle_state_TH2(ctx: Channel, message_length: int, ctrl_byte: int) -> None:
from apps.thp.credential_manager import decode_credential, validate_credential
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "handle_state_TH2")
if not control_byte.is_handshake_comp_req(ctrl_byte):
raise ThpError("Message received is not a handshake completion request!")
if ctx.handshake is None:
raise Exception("Handshake object is not prepared. Retry handshake.")
if not config.is_unlocked():
raise ThpDeviceLockedError
buffer = memory_manager.get_existing_read_buffer(ctx.get_channel_id_int())
# if buffer is BufferError:
# pass # TODO handle
host_encrypted_static_pubkey = buffer[
INIT_HEADER_LENGTH : INIT_HEADER_LENGTH + KEY_LENGTH + TAG_LENGTH
]
handshake_completion_request_noise_payload = buffer[
INIT_HEADER_LENGTH + KEY_LENGTH + TAG_LENGTH : message_length - CHECKSUM_LENGTH
]
ctx.handshake.handle_th2_crypto(
host_encrypted_static_pubkey, handshake_completion_request_noise_payload
)
ctx.channel_cache.set(CHANNEL_KEY_RECEIVE, ctx.handshake.key_receive)
ctx.channel_cache.set(CHANNEL_KEY_SEND, ctx.handshake.key_send)
ctx.channel_cache.set(CHANNEL_HANDSHAKE_HASH, ctx.handshake.h)
ctx.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, 0)
ctx.channel_cache.set_int(CHANNEL_NONCE_SEND, 1)
noise_payload = _decode_message(
buffer[
INIT_HEADER_LENGTH
+ KEY_LENGTH
+ TAG_LENGTH : message_length
- CHECKSUM_LENGTH
- TAG_LENGTH
],
0,
"ThpHandshakeCompletionReqNoisePayload",
)
if TYPE_CHECKING:
assert ThpHandshakeCompletionReqNoisePayload.is_type_of(noise_payload)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"host static pubkey: %s, noise payload: %s",
utils.get_bytes_as_str(host_encrypted_static_pubkey),
utils.get_bytes_as_str(handshake_completion_request_noise_payload),
)
# key is decoded in handshake._handle_th2_crypto
host_static_pubkey = host_encrypted_static_pubkey[:PUBKEY_LENGTH]
paired: bool = False
trezor_state = _TREZOR_STATE_UNPAIRED
if noise_payload.host_pairing_credential is not None:
try: # TODO change try-except for something better
credential = decode_credential(noise_payload.host_pairing_credential)
paired = validate_credential(
credential,
host_static_pubkey,
)
if paired:
trezor_state = _TREZOR_STATE_PAIRED
ctx.credential = credential
else:
ctx.credential = None
except DataError as e:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.exception(__name__, e)
pass
# send hanshake completion response
ctx.write_handshake_message(
HANDSHAKE_COMP_RES,
ctx.handshake.get_handshake_completion_response(trezor_state),
)
ctx.handshake = None
if paired:
ctx.set_channel_state(ChannelState.TC1)
else:
ctx.set_channel_state(ChannelState.TP0)
async def _handle_state_ENCRYPTED_TRANSPORT(ctx: Channel, message_length: int) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "handle_state_ENCRYPTED_TRANSPORT")
ctx.decrypt_buffer(message_length)
buffer = memory_manager.get_existing_read_buffer(ctx.get_channel_id_int())
# if buffer is BufferError:
# pass # TODO handle
session_id, message_type = ustruct.unpack(
">BH", memoryview(buffer)[INIT_HEADER_LENGTH:]
)
if session_id not in ctx.sessions:
s = session_manager.get_session_from_cache(ctx, session_id)
if s is None:
s = SeedlessSessionContext(ctx, session_id)
ctx.sessions[session_id] = s
loop.schedule(s.handle())
elif ctx.sessions[session_id].get_session_state() is SessionState.UNALLOCATED:
raise ThpUnallocatedSessionError(session_id)
s = ctx.sessions[session_id]
update_session_last_used(s.channel_id, (s.session_id).to_bytes(1, "big"))
s.incoming_message.put(
Message(
message_type,
buffer[
INIT_HEADER_LENGTH
+ MESSAGE_TYPE_LENGTH
+ SESSION_ID_LENGTH : message_length
- CHECKSUM_LENGTH
- TAG_LENGTH
],
)
)
async def _handle_pairing(ctx: Channel, message_length: int) -> None:
from .pairing_context import PairingContext
if ctx.connection_context is None:
ctx.connection_context = PairingContext(ctx)
loop.schedule(ctx.connection_context.handle())
ctx.decrypt_buffer(message_length)
buffer = memory_manager.get_existing_read_buffer(ctx.get_channel_id_int())
# if buffer is BufferError:
# pass # TODO handle
message_type = ustruct.unpack(
">H", buffer[INIT_HEADER_LENGTH + SESSION_ID_LENGTH :]
)[0]
ctx.connection_context.incoming_message.put(
Message(
message_type,
buffer[
INIT_HEADER_LENGTH
+ MESSAGE_TYPE_LENGTH
+ SESSION_ID_LENGTH : message_length
- CHECKSUM_LENGTH
- TAG_LENGTH
],
)
)
def _should_have_ctrl_byte_encrypted_transport(ctx: Channel) -> bool:
if ctx.get_channel_state() in [
ChannelState.UNALLOCATED,
ChannelState.TH1,
ChannelState.TH2,
]:
return False
return True
def _decode_message(
buffer: bytes, msg_type: int, message_name: str | None = None
) -> protobuf.MessageType:
if __debug__:
log.debug(__name__, "decode message")
if message_name is not None:
expected_type = protobuf.type_for_name(message_name)
else:
expected_type = protobuf.type_for_wire(msg_type)
return message_handler.wrap_protobuf_load(buffer, expected_type)
def _is_channel_state_pairing(state: int) -> bool:
if state in (
ChannelState.TP0,
ChannelState.TP1,
ChannelState.TP2,
ChannelState.TP3,
ChannelState.TP4,
ChannelState.TC1,
):
return True
return False

View File

@ -0,0 +1,169 @@
from typing import TYPE_CHECKING
from storage import cache_thp
from storage.cache_common import InvalidSessionError
from storage.cache_thp import SessionThpCache
from trezor import log, loop, protobuf, utils
from trezor.wire import message_handler, protocol_common
from trezor.wire.context import UnexpectedMessageException
from trezor.wire.message_handler import failure
from ..protocol_common import Context, Message
from . import SessionState
if TYPE_CHECKING:
from typing import Awaitable, Container
from storage.cache_common import DataCache
from .channel import Channel
pass
_EXIT_LOOP = True
_REPEAT_LOOP = False
if __debug__:
from trezor.utils import get_bytes_as_str
class GenericSessionContext(Context):
def __init__(self, channel: Channel, session_id: int) -> None:
super().__init__(channel.iface, channel.channel_id)
self.channel: Channel = channel
self.session_id: int = session_id
self.incoming_message = loop.mailbox()
async def handle(self) -> None:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__,
"handle - start (channel_id (bytes): %s, session_id: %d)",
get_bytes_as_str(self.channel_id),
self.session_id,
)
next_message: Message | None = None
while True:
message = next_message
next_message = None
try:
if await self._handle_message(message):
loop.schedule(self.handle())
return
except UnexpectedMessageException as unexpected:
# The workflow was interrupted by an unexpected message. We need to
# process it as if it was a new message...
next_message = unexpected.msg
continue
except Exception as exc:
# Log and try again.
if __debug__:
log.exception(__name__, exc)
async def _handle_message(
self,
next_message: Message | None,
) -> bool:
try:
if next_message is not None:
# Process the message from previous run.
message = next_message
next_message = None
else:
# Wait for a new message from wire
message = await self.incoming_message
except protocol_common.WireError as e:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.exception(__name__, e)
await self.write(failure(e))
return _REPEAT_LOOP
await message_handler.handle_single_message(self, message)
return _EXIT_LOOP
async def read(
self,
expected_types: Container[int],
expected_type: type[protobuf.MessageType] | None = None,
) -> protobuf.MessageType:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
exp_type: str = str(expected_type)
if expected_type is not None:
exp_type = expected_type.MESSAGE_NAME
log.debug(
__name__,
"Read - with expected types %s and expected type %s",
str(expected_types),
exp_type,
)
message: Message = await self.incoming_message
if message.type not in expected_types:
if __debug__:
log.debug(
__name__,
"EXPECTED TYPES: %s\nRECEIVED TYPE: %s",
str(expected_types),
str(message.type),
)
raise UnexpectedMessageException(message)
if expected_type is None:
expected_type = protobuf.type_for_wire(message.type)
return message_handler.wrap_protobuf_load(message.data, expected_type)
async def write(self, msg: protobuf.MessageType) -> None:
return await self.channel.write(msg, self.session_id)
def write_force(self, msg: protobuf.MessageType) -> Awaitable[None]:
return self.channel.write(msg, self.session_id, force=True)
def get_session_state(self) -> SessionState: ...
class SeedlessSessionContext(GenericSessionContext):
def __init__(self, channel_ctx: Channel, session_id: int) -> None:
super().__init__(channel_ctx, session_id)
def get_session_state(self) -> SessionState:
return SessionState.SEEDLESS
@property
def cache(self) -> DataCache:
raise InvalidSessionError
class SessionContext(GenericSessionContext):
def __init__(self, channel_ctx: Channel, session_cache: SessionThpCache) -> None:
if channel_ctx.channel_id != session_cache.channel_id:
raise Exception(
"The session has different channel id than the provided channel context!"
)
session_id = int.from_bytes(session_cache.session_id, "big")
super().__init__(channel_ctx, session_id)
self.session_cache = session_cache
# ACCESS TO SESSION DATA
def get_session_state(self) -> SessionState:
state = int.from_bytes(self.session_cache.state, "big")
return SessionState(state)
def set_session_state(self, state: SessionState) -> None:
self.session_cache.state = bytearray(state.to_bytes(1, "big"))
def release(self) -> None:
if self.session_cache is not None:
cache_thp.clear_session(self.session_cache)
# ACCESS TO CACHE
@property
def cache(self) -> DataCache:
return self.session_cache

View File

@ -0,0 +1,51 @@
from typing import TYPE_CHECKING
from storage import cache_thp
from .session_context import (
GenericSessionContext,
SeedlessSessionContext,
SessionContext,
)
if TYPE_CHECKING:
from .channel import Channel
def get_new_session_context(
channel_ctx: Channel,
session_id: int,
) -> SessionContext:
session_cache = cache_thp.create_or_replace_session(
channel=channel_ctx.channel_cache,
session_id=session_id.to_bytes(1, "big"),
)
return SessionContext(channel_ctx, session_cache)
def get_new_seedless_session_ctx(
channel_ctx: Channel, session_id: int
) -> SeedlessSessionContext:
"""
Creates new `SeedlessSessionContext` that is not backed by a cache entry.
Seed cannot be derived with this type of session.
"""
return SeedlessSessionContext(channel_ctx, session_id)
def get_session_from_cache(
channel_ctx: Channel, session_id: int
) -> GenericSessionContext | None:
"""
Returns a `SessionContext` (or `SeedlessSessionContext`) reconstructed from a cache or `None` if backing cache is not found.
"""
session_id_bytes = session_id.to_bytes(1, "big")
session_cache = cache_thp.get_allocated_session(
channel_ctx.channel_id, session_id_bytes
)
if session_cache is None:
return None
elif cache_thp.is_seedless_session(session_cache):
return SeedlessSessionContext(channel_ctx, session_id)
return SessionContext(channel_ctx, session_cache)

View File

@ -0,0 +1,165 @@
import ustruct
from micropython import const
from typing import TYPE_CHECKING
from storage.cache_thp import BROADCAST_CHANNEL_ID
from trezor import io, log, loop, utils
from . import (
CHANNEL_ALLOCATION_REQ,
CODEC_V1,
ChannelState,
PacketHeader,
ThpError,
ThpErrorType,
channel_manager,
checksum,
control_byte,
get_channel_allocation_response,
writer,
)
from .channel import Channel
from .checksum import CHECKSUM_LENGTH
from .writer import (
INIT_HEADER_LENGTH,
MAX_PAYLOAD_LEN,
write_payload_to_wire_and_add_checksum,
)
if TYPE_CHECKING:
from trezorio import WireInterface
_CID_REQ_PAYLOAD_LENGTH = const(12)
_CHANNELS: dict[int, Channel] = {}
async def thp_main_loop(iface: WireInterface) -> None:
global _CHANNELS
_CHANNELS = channel_manager.load_cached_channels()
read = loop.wait(iface.iface_num() | io.POLL_READ)
packet = bytearray(iface.RX_PACKET_LEN)
while True:
try:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(__name__, "thp_main_loop")
packet_len = await read
assert packet_len == len(packet)
iface.read(packet, 0)
if _get_ctrl_byte(packet) == CODEC_V1:
await _handle_codec_v1(iface, packet)
continue
cid = ustruct.unpack(">BH", packet)[1]
if cid == BROADCAST_CHANNEL_ID:
await _handle_broadcast(iface, packet)
continue
if cid in _CHANNELS:
await _handle_allocated(iface, cid, packet)
else:
await _handle_unallocated(iface, cid, packet)
except ThpError as e:
if __debug__:
log.exception(__name__, e)
async def _handle_codec_v1(iface: WireInterface, packet: bytes) -> None:
# If the received packet is not an initial codec_v1 packet, do not send error message
if not packet[1:3] == b"##":
return
if __debug__:
log.debug(__name__, "Received codec_v1 message, returning error")
error_message = _get_codec_v1_error_message()
await writer.write_packet_to_wire(iface, error_message)
async def _handle_broadcast(iface: WireInterface, packet: utils.BufferType) -> None:
if _get_ctrl_byte(packet) != CHANNEL_ALLOCATION_REQ:
raise ThpError("Unexpected ctrl_byte in a broadcast channel packet")
if __debug__:
log.debug(__name__, "Received valid message on the broadcast channel")
length, nonce = ustruct.unpack(">H8s", packet[3:])
payload = _get_buffer_for_payload(length, packet[5:], _CID_REQ_PAYLOAD_LENGTH)
if not checksum.is_valid(
payload[-4:],
packet[: _CID_REQ_PAYLOAD_LENGTH + INIT_HEADER_LENGTH - CHECKSUM_LENGTH],
):
raise ThpError("Checksum is not valid")
new_channel: Channel = channel_manager.create_new_channel(iface)
cid = int.from_bytes(new_channel.channel_id, "big")
_CHANNELS[cid] = new_channel
response_data = get_channel_allocation_response(
nonce, new_channel.channel_id, iface
)
response_header = PacketHeader.get_channel_allocation_response_header(
len(response_data) + CHECKSUM_LENGTH,
)
if __debug__:
log.debug(__name__, "New channel allocated with id %d", cid)
await write_payload_to_wire_and_add_checksum(iface, response_header, response_data)
async def _handle_allocated(
iface: WireInterface, cid: int, packet: utils.BufferType
) -> None:
channel = _CHANNELS[cid]
if channel is None:
await _handle_unallocated(iface, cid, packet)
raise ThpError("Invalid state of a channel")
if channel.iface is not iface:
# TODO send error message to wire
raise ThpError("Channel has different WireInterface")
if channel.get_channel_state() != ChannelState.UNALLOCATED:
x = channel.receive_packet(packet)
if x is not None:
await x
async def _handle_unallocated(iface: WireInterface, cid: int, packet: bytes) -> None:
if control_byte.is_continuation(_get_ctrl_byte(packet)):
return
data = (ThpErrorType.UNALLOCATED_CHANNEL).to_bytes(1, "big")
header = PacketHeader.get_error_header(cid, len(data) + CHECKSUM_LENGTH)
await write_payload_to_wire_and_add_checksum(iface, header, data)
def _get_buffer_for_payload(
payload_length: int,
existing_buffer: utils.BufferType,
max_length: int = MAX_PAYLOAD_LEN,
) -> utils.BufferType:
if payload_length > max_length:
raise ThpError("Message too large")
if payload_length > len(existing_buffer):
try:
new_buffer = bytearray(payload_length)
except MemoryError:
raise ThpError("Message too large")
return new_buffer
return _reuse_existing_buffer(payload_length, existing_buffer)
def _reuse_existing_buffer(
payload_length: int, existing_buffer: utils.BufferType
) -> utils.BufferType:
return memoryview(existing_buffer)[:payload_length]
def _get_ctrl_byte(packet: bytes) -> int:
return packet[0]
def _get_codec_v1_error_message() -> bytes:
# Codec_v1 magic constant "?##" + Failure message type + msg_size
# + msg_data (code = "Failure_InvalidProtocol") + padding to 64 B
ERROR_MSG = b"\x3f\x23\x23\x00\x03\x00\x00\x00\x14\x08\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
return ERROR_MSG

View File

@ -0,0 +1,54 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import loop
from .writer import write_payload_to_wire_and_add_checksum
if TYPE_CHECKING:
from . import PacketHeader
from .channel import Channel
MAX_RETRANSMISSION_COUNT = const(50)
MIN_RETRANSMISSION_COUNT = const(2)
class TransmissionLoop:
def __init__(
self, channel: Channel, header: PacketHeader, transport_payload: bytes
) -> None:
self.channel: Channel = channel
self.header: PacketHeader = header
self.transport_payload: bytes = transport_payload
self.wait_task: loop.spawn | None = None
self.min_retransmisson_count_achieved: bool = False
async def start(
self, max_retransmission_count: int = MAX_RETRANSMISSION_COUNT
) -> None:
self.min_retransmisson_count_achieved = False
for i in range(max_retransmission_count):
if i >= MIN_RETRANSMISSION_COUNT:
self.min_retransmisson_count_achieved = True
await write_payload_to_wire_and_add_checksum(
self.channel.iface, self.header, self.transport_payload
)
self.wait_task = loop.spawn(self._wait(i))
try:
await self.wait_task
except loop.TaskClosed:
self.wait_task = None
break
def stop_immediately(self) -> None:
if self.wait_task is not None:
self.wait_task.close()
self.wait_task = None
async def _wait(self, counter: int = 0) -> None:
timeout_ms = round(10200 - 1010000 / (counter + 100))
await loop.sleep(timeout_ms)
def __del__(self) -> None:
self.stop_immediately()

View File

@ -0,0 +1,91 @@
from micropython import const
from trezorcrypto import crc
from typing import TYPE_CHECKING
from trezor import io, log, loop, utils
from . import PacketHeader
INIT_HEADER_LENGTH = const(5)
CONT_HEADER_LENGTH = const(3)
CHECKSUM_LENGTH = const(4)
MAX_PAYLOAD_LEN = const(60000)
MESSAGE_TYPE_LENGTH = const(2)
if TYPE_CHECKING:
from trezorio import WireInterface
from typing import Awaitable, Sequence
def write_payload_to_wire_and_add_checksum(
iface: WireInterface, header: PacketHeader, transport_payload: bytes
) -> Awaitable[None]:
header_checksum: int = crc.crc32(header.to_bytes())
checksum: bytes = crc.crc32(transport_payload, header_checksum).to_bytes(
CHECKSUM_LENGTH, "big"
)
data = (transport_payload, checksum)
return write_payloads_to_wire(iface, header, data)
async def write_payloads_to_wire(
iface: WireInterface, header: PacketHeader, data: Sequence[bytes]
) -> None:
n_of_data = len(data)
total_length = sum(len(item) for item in data)
current_data_idx = 0
current_data_offset = 0
packet = bytearray(iface.TX_PACKET_LEN)
header.pack_to_init_buffer(packet)
packet_offset: int = INIT_HEADER_LENGTH
packet_number = 0
nwritten = 0
while nwritten < total_length:
if packet_number == 1:
header.pack_to_cont_buffer(packet)
if packet_number >= 1 and nwritten >= total_length - iface.TX_PACKET_LEN:
packet[:] = bytearray(iface.TX_PACKET_LEN)
header.pack_to_cont_buffer(packet)
while True:
n = utils.memcpy(
packet, packet_offset, data[current_data_idx], current_data_offset
)
packet_offset += n
current_data_offset += n
nwritten += n
if packet_offset < iface.TX_PACKET_LEN:
current_data_idx += 1
current_data_offset = 0
if current_data_idx >= n_of_data:
break
elif packet_offset == iface.TX_PACKET_LEN:
break
else:
raise Exception("Should not happen!!!")
packet_number += 1
packet_offset = CONT_HEADER_LENGTH
# write packet to wire (in-lined)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__, "write_packet_to_wire: %s", utils.get_bytes_as_str(packet)
)
written_by_iface: int = 0
while written_by_iface < len(packet):
await loop.wait(iface.iface_num() | io.POLL_WRITE)
written_by_iface = iface.write(packet)
async def write_packet_to_wire(iface: WireInterface, packet: bytes) -> None:
while True:
await loop.wait(iface.iface_num() | io.POLL_WRITE)
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.debug(
__name__, "write_packet_to_wire: %s", utils.get_bytes_as_str(packet)
)
n_written = iface.write(packet)
if n_written == len(packet):
return

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import storage.cache as storage_cache import storage.cache as storage_cache
from trezor import log, loop from trezor import log, loop
from trezor.enums import MessageType from trezor.enums import MessageType, ThpMessageType
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable from typing import Callable
@ -17,18 +17,30 @@ if __debug__:
from trezor import utils from trezor import utils
if utils.USE_THP:
ALLOW_WHILE_LOCKED = ( ALLOW_WHILE_LOCKED = (
MessageType.Initialize, ThpMessageType.ThpCreateNewSession,
MessageType.EndSession, MessageType.EndSession,
MessageType.GetFeatures, MessageType.GetFeatures,
MessageType.Cancel, MessageType.Cancel,
MessageType.LockDevice, MessageType.LockDevice,
MessageType.DoPreauthorized, MessageType.DoPreauthorized,
MessageType.WipeDevice, MessageType.WipeDevice,
MessageType.SetBusy, MessageType.SetBusy,
MessageType.Ping, MessageType.Ping,
) )
else:
ALLOW_WHILE_LOCKED = (
MessageType.Initialize,
MessageType.EndSession,
MessageType.GetFeatures,
MessageType.Cancel,
MessageType.LockDevice,
MessageType.DoPreauthorized,
MessageType.WipeDevice,
MessageType.SetBusy,
MessageType.Ping,
)
# Set of workflow tasks. Multiple workflows can be running at the same time. # Set of workflow tasks. Multiple workflows can be running at the same time.

View File

@ -0,0 +1,50 @@
from trezor.loop import wait
class MockHID:
TX_PACKET_LEN = 64
RX_PACKET_LEN = 64
def __init__(self, num):
self.num = num
self.data = []
self.packet = None
def pad_packet(self, data):
if len(data) > self.RX_PACKET_LEN:
raise Exception("Too long packet")
padding_length = self.RX_PACKET_LEN - len(data)
return data + b"\x00" * padding_length
def iface_num(self):
return self.num
def write(self, msg):
self.data.append(bytearray(msg))
return len(msg)
def mock_read(self, packet, gen):
self.packet = self.pad_packet(packet)
return gen.send(self.RX_PACKET_LEN)
def read(self, buffer, offset=0):
if self.packet is None:
raise Exception("No packet to read")
if offset > len(buffer):
raise Exception("Offset out of bounds")
buffer_space = len(buffer) - offset
if len(self.packet) > buffer_space:
raise Exception("Buffer too small")
else:
end = offset + len(self.packet)
buffer[offset:end] = self.packet
read = len(self.packet)
self.packet = None
return read
def wait_object(self, mode):
return wait(mode | self.num)

42
core/tests/myTests.sh Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
declare -a results
declare -i passed=0 failed=0 exit_code=0
declare COLOR_GREEN='\e[32m' COLOR_RED='\e[91m' COLOR_RESET='\e[39m'
MICROPYTHON="${MICROPYTHON:-../build/unix/trezor-emu-core -X heapsize=2M}"
print_summary() {
echo
echo 'Summary:'
echo '-------------------'
printf '%b\n' "${results[@]}"
if [ $exit_code == 0 ]; then
echo -e "${COLOR_GREEN}PASSED:${COLOR_RESET} $passed/$num_of_tests tests OK!"
else
echo -e "${COLOR_RED}FAILED:${COLOR_RESET} $failed/$num_of_tests tests failed!"
fi
}
trap 'print_summary; echo -e "${COLOR_RED}Interrupted by user!${COLOR_RESET}"; exit 1' SIGINT
cd $(dirname $0)
[ -z "$*" ] && tests=(test_trezor.wire.t*.py ) || tests=($*)
declare -i num_of_tests=${#tests[@]}
for test_case in ${tests[@]}; do
echo ${MICROPYTHON}
echo ${test_case}
echo
if $MICROPYTHON $test_case; then
results+=("${COLOR_GREEN}OK:${COLOR_RESET} $test_case")
((passed++))
else
results+=("${COLOR_RED}FAIL:${COLOR_RESET} $test_case")
((failed++))
exit_code=1
fi
done
print_summary
exit $exit_code

View File

@ -1,4 +1,5 @@
from common import H_, await_result, unittest # isort:skip # flake8: noqa: F403,F405
from common import * # isort:skip
import storage.cache_codec import storage.cache_codec
from trezor import wire from trezor import wire
@ -20,11 +21,25 @@ from apps.bitcoin.sign_tx.bitcoin import Bitcoin
from apps.bitcoin.sign_tx.tx_info import TxInfo from apps.bitcoin.sign_tx.tx_info import TxInfo
from apps.common import coins from apps.common import coins
if utils.USE_THP:
import thp_common
else:
import storage.cache_codec
from trezor.wire.codec.codec_context import CodecContext
class TestApprover(unittest.TestCase): class TestApprover(unittest.TestCase):
if utils.USE_THP:
def setUpClass(self): def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64)) if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
@ -54,7 +69,8 @@ class TestApprover(unittest.TestCase):
coin_name=self.coin.coin_name, coin_name=self.coin.coin_name,
script_type=InputScriptType.SPENDTAPROOT, script_type=InputScriptType.SPENDTAPROOT,
) )
storage.cache_codec.start_session() if not utils.USE_THP:
storage.cache_codec.start_session()
def make_coinjoin_request(self, inputs): def make_coinjoin_request(self, inputs):
return CoinJoinRequest( return CoinJoinRequest(

View File

@ -1,23 +1,38 @@
from common import H_, unittest # isort:skip # flake8: noqa: F403,F405
from common import * # isort:skip
import storage.cache_codec import storage.cache_codec
from trezor.enums import InputScriptType from trezor.enums import InputScriptType
from trezor.messages import AuthorizeCoinJoin, GetOwnershipProof, SignTx from trezor.messages import AuthorizeCoinJoin, GetOwnershipProof, SignTx
from trezor.wire import context from trezor.wire import context
from trezor.wire.codec.codec_context import CodecContext
from apps.bitcoin.authorization import CoinJoinAuthorization from apps.bitcoin.authorization import CoinJoinAuthorization
from apps.common import coins from apps.common import coins
_ROUND_ID_LEN = 32 _ROUND_ID_LEN = 32
if utils.USE_THP:
import thp_common
else:
import storage.cache_codec
from trezor.wire.codec.codec_context import CodecContext
class TestAuthorization(unittest.TestCase): class TestAuthorization(unittest.TestCase):
coin = coins.by_name("Bitcoin") coin = coins.by_name("Bitcoin")
def setUpClass(self): if utils.USE_THP:
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUpClass(self):
if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
@ -34,7 +49,8 @@ class TestAuthorization(unittest.TestCase):
) )
self.authorization = CoinJoinAuthorization(self.msg_auth) self.authorization = CoinJoinAuthorization(self.msg_auth)
storage.cache_codec.start_session() if not utils.USE_THP:
storage.cache_codec.start_session()
def test_ownership_proof_account_depth_mismatch(self): def test_ownership_proof_account_depth_mismatch(self):
# Account depth mismatch. # Account depth mismatch.

View File

@ -1,7 +1,7 @@
# flake8: noqa: F403,F405 # flake8: noqa: F403,F405
from common import * # isort:skip from common import * # isort:skip
from storage import cache_codec, cache_common from storage import cache_common
from trezor import wire from trezor import wire
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.wire import context from trezor.wire import context
@ -9,20 +9,38 @@ from trezor.wire.codec.codec_context import CodecContext
from apps.bitcoin.keychain import _get_coin_by_name, _get_keychain_for_coin from apps.bitcoin.keychain import _get_coin_by_name, _get_keychain_for_coin
if utils.USE_THP:
import thp_common
else:
from storage import cache_codec
class TestBitcoinKeychain(unittest.TestCase): class TestBitcoinKeychain(unittest.TestCase):
def setUpClass(self): if utils.USE_THP:
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUpClass(self):
if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
def setUp(self):
seed = bip39.seed(" ".join(["all"] * 12), "")
context.cache_set(cache_common.APP_COMMON_SEED, seed)
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def test_bitcoin(self): def test_bitcoin(self):
coin = _get_coin_by_name("Bitcoin") coin = _get_coin_by_name("Bitcoin")
keychain = await_result(_get_keychain_for_coin(coin)) keychain = await_result(_get_keychain_for_coin(coin))
@ -98,18 +116,30 @@ class TestBitcoinKeychain(unittest.TestCase):
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestAltcoinKeychains(unittest.TestCase): class TestAltcoinKeychains(unittest.TestCase):
if utils.USE_THP:
def setUpClass(self): def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64)) if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
def setUp(self):
seed = bip39.seed(" ".join(["all"] * 12), "")
context.cache_set(cache_common.APP_COMMON_SEED, seed)
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def test_bcash(self): def test_bcash(self):
coin = _get_coin_by_name("Bcash") coin = _get_coin_by_name("Bcash")
keychain = await_result(_get_keychain_for_coin(coin)) keychain = await_result(_get_keychain_for_coin(coin))

View File

@ -2,7 +2,7 @@
from common import * # isort:skip from common import * # isort:skip
from mock_storage import mock_storage from mock_storage import mock_storage
from storage import cache, cache_codec, cache_common from storage import cache, cache_common
from trezor import wire from trezor import wire
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.enums import SafetyCheckLevel from trezor.enums import SafetyCheckLevel
@ -13,18 +13,32 @@ from apps.common import safety_checks
from apps.common.keychain import Keychain, LRUCache, get_keychain, with_slip44_keychain from apps.common.keychain import Keychain, LRUCache, get_keychain, with_slip44_keychain
from apps.common.paths import PATTERN_SEP5, PathSchema from apps.common.paths import PATTERN_SEP5, PathSchema
if utils.USE_THP:
import thp_common
if not utils.USE_THP:
from storage import cache_codec
class TestKeychain(unittest.TestCase): class TestKeychain(unittest.TestCase):
def setUpClass(self): if utils.USE_THP:
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUpClass(self):
if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUp(self):
cache_codec.start_session()
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
def setUp(self):
cache_codec.start_session()
def tearDown(self): def tearDown(self):
cache.clear_all() cache.clear_all()

View File

@ -3,7 +3,7 @@ from common import * # isort:skip
import unittest import unittest
from storage import cache_codec, cache_common from storage import cache_common
from trezor import wire from trezor import wire
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.wire import context from trezor.wire import context
@ -12,6 +12,12 @@ from trezor.wire.codec.codec_context import CodecContext
from apps.common.keychain import get_keychain from apps.common.keychain import get_keychain
from apps.common.paths import HARDENED from apps.common.paths import HARDENED
if utils.USE_THP:
import thp_common
else:
from storage import cache_codec
if not utils.BITCOIN_ONLY: if not utils.BITCOIN_ONLY:
from ethereum_common import encode_network, make_network from ethereum_common import encode_network, make_network
from trezor.messages import ( from trezor.messages import (
@ -74,17 +80,30 @@ class TestEthereumKeychain(unittest.TestCase):
addr, addr,
) )
def setUpClass(self): if utils.USE_THP:
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUpClass(self):
if __debug__:
thp_common.suppres_debug_log()
thp_common.prepare_context()
def setUp(self):
seed = bip39.seed(" ".join(["all"] * 12), "")
context.cache_set(cache_common.APP_COMMON_SEED, seed)
else:
def setUpClass(self):
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def tearDownClass(self): def tearDownClass(self):
context.CURRENT_CONTEXT = None context.CURRENT_CONTEXT = None
def setUp(self):
cache_codec.start_session()
seed = bip39.seed(" ".join(["all"] * 12), "")
cache_codec.get_active_session().set(cache_common.APP_COMMON_SEED, seed)
def from_address_n(self, address_n): def from_address_n(self, address_n):
slip44 = _slip44_from_address_n(address_n) slip44 = _slip44_from_address_n(address_n)
network = make_network(slip44=slip44) network = make_network(slip44=slip44)

View File

@ -48,16 +48,28 @@ class TestTrezorHostProtocolCredentialManager(unittest.TestCase):
cred_3 = _issue_credential(HOST_NAME_2, DUMMY_KEY_1) cred_3 = _issue_credential(HOST_NAME_2, DUMMY_KEY_1)
self.assertNotEqual(cred_1, cred_3) self.assertNotEqual(cred_1, cred_3)
self.assertTrue(credential_manager.validate_credential(cred_1, DUMMY_KEY_1)) self.assertTrue(
self.assertTrue(credential_manager.validate_credential(cred_3, DUMMY_KEY_1)) credential_manager.decode_and_validate_credential(cred_1, DUMMY_KEY_1)
self.assertFalse(credential_manager.validate_credential(cred_1, DUMMY_KEY_2)) )
self.assertTrue(
credential_manager.decode_and_validate_credential(cred_3, DUMMY_KEY_1)
)
self.assertFalse(
credential_manager.decode_and_validate_credential(cred_1, DUMMY_KEY_2)
)
credential_manager.invalidate_cred_auth_key() credential_manager.invalidate_cred_auth_key()
cred_4 = _issue_credential(HOST_NAME_1, DUMMY_KEY_1) cred_4 = _issue_credential(HOST_NAME_1, DUMMY_KEY_1)
self.assertNotEqual(cred_1, cred_4) self.assertNotEqual(cred_1, cred_4)
self.assertFalse(credential_manager.validate_credential(cred_1, DUMMY_KEY_1)) self.assertFalse(
self.assertFalse(credential_manager.validate_credential(cred_3, DUMMY_KEY_1)) credential_manager.decode_and_validate_credential(cred_1, DUMMY_KEY_1)
self.assertTrue(credential_manager.validate_credential(cred_4, DUMMY_KEY_1)) )
self.assertFalse(
credential_manager.decode_and_validate_credential(cred_3, DUMMY_KEY_1)
)
self.assertTrue(
credential_manager.decode_and_validate_credential(cred_4, DUMMY_KEY_1)
)
def test_protobuf_encoding(self): def test_protobuf_encoding(self):
""" """

View File

@ -1,241 +1,560 @@
# flake8: noqa: F403,F405 # flake8: noqa: F403,F405
from common import * # isort:skip from common import * # isort:skip
from mock_storage import mock_storage
from storage import cache, cache_codec, cache_common
from trezor.messages import EndSession, Initialize
from trezor.wire import context
from trezor.wire.codec.codec_context import CodecContext
from apps.base import handle_EndSession, handle_Initialize
from apps.common.cache import stored, stored_async
KEY = 0 KEY = 0
if utils.USE_THP:
import thp_common
from mock_wire_interface import MockHID
from storage import cache, cache_thp
from trezor.wire.thp import ChannelState
from trezor.wire.thp.session_context import SessionContext
# Function moved from cache.py, as it was not used there _PROTOCOL_CACHE = cache_thp
def is_session_started() -> bool:
return cache_codec._active_session_idx is not None else:
from mock_storage import mock_storage
from storage import cache, cache_codec
from trezor.messages import EndSession, Initialize
from apps.base import handle_EndSession
_PROTOCOL_CACHE = cache_codec
def is_session_started() -> bool:
return cache_codec.get_active_session() is not None
def get_active_session():
return cache_codec.get_active_session()
class TestStorageCache(unittest.TestCase): class TestStorageCache(unittest.TestCase):
def setUpClass(self): if utils.USE_THP:
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def tearDownClass(self): def setUpClass(self):
context.CURRENT_CONTEXT = None if __debug__:
thp_common.suppres_debug_log()
super().__init__()
def setUp(self): def setUp(self):
cache.clear_all() self.interface = MockHID(0xDEADBEEF)
cache.clear_all()
def test_start_session(self): def test_new_channel_and_session(self):
session_id_a = cache_codec.start_session() channel = thp_common.get_new_channel(self.interface)
self.assertIsNotNone(session_id_a)
session_id_b = cache_codec.start_session()
self.assertNotEqual(session_id_a, session_id_b)
cache.clear_all() # Assert that channel is created without any sessions
with self.assertRaises(cache_common.InvalidSessionError): self.assertEqual(len(channel.sessions), 0)
context.cache_set(KEY, "something")
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_get(KEY)
def test_end_session(self): cid_1 = channel.channel_id
session_id = cache_codec.start_session() session_cache_1 = cache_thp.create_or_replace_session(
self.assertTrue(is_session_started()) channel.channel_cache, b"\x01"
context.cache_set(KEY, b"A") )
cache_codec.end_current_session() session_1 = SessionContext(channel, session_cache_1)
self.assertFalse(is_session_started()) self.assertEqual(session_1.channel_id, cid_1)
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
# ending an ended session should be a no-op session_cache_2 = cache_thp.create_or_replace_session(
cache_codec.end_current_session() channel.channel_cache, b"\x02"
self.assertFalse(is_session_started()) )
session_2 = SessionContext(channel, session_cache_2)
self.assertEqual(session_2.channel_id, cid_1)
self.assertEqual(session_1.channel_id, session_2.channel_id)
self.assertNotEqual(session_1.session_id, session_2.session_id)
session_id_a = cache_codec.start_session(session_id) channel_2 = thp_common.get_new_channel(self.interface)
# original session no longer exists cid_2 = channel_2.channel_id
self.assertNotEqual(session_id_a, session_id) self.assertNotEqual(cid_1, cid_2)
# original session data no longer exists
self.assertIsNone(context.cache_get(KEY))
# create a new session session_cache_3 = cache_thp.create_or_replace_session(
session_id_b = cache_codec.start_session() channel_2.channel_cache, b"\x01"
# switch back to original session )
session_id = cache_codec.start_session(session_id_a) session_3 = SessionContext(channel_2, session_cache_3)
self.assertEqual(session_id, session_id_a) self.assertEqual(session_3.channel_id, cid_2)
# end original session
cache_codec.end_current_session()
# switch back to B
session_id = cache_codec.start_session(session_id_b)
self.assertEqual(session_id, session_id_b)
def test_session_queue(self): # Sessions 1 and 3 should have different channel_id, but the same session_id
session_id = cache_codec.start_session() self.assertNotEqual(session_1.channel_id, session_3.channel_id)
self.assertEqual(cache_codec.start_session(session_id), session_id) self.assertEqual(session_1.session_id, session_3.session_id)
context.cache_set(KEY, b"A")
for _ in range(cache_codec._MAX_SESSIONS_COUNT): self.assertEqual(cache_thp._SESSIONS[0], session_cache_1)
self.assertNotEqual(cache_thp._SESSIONS[0], session_cache_2)
self.assertEqual(cache_thp._SESSIONS[0].channel_id, session_1.channel_id)
# Check that session data IS in cache for created sessions ONLY
for i in range(3):
self.assertNotEqual(cache_thp._SESSIONS[i].channel_id, b"")
self.assertNotEqual(cache_thp._SESSIONS[i].session_id, b"")
self.assertNotEqual(cache_thp._SESSIONS[i].last_usage, 0)
for i in range(3, cache_thp._MAX_SESSIONS_COUNT):
self.assertEqual(cache_thp._SESSIONS[i].channel_id, b"")
self.assertEqual(cache_thp._SESSIONS[i].session_id, b"")
self.assertEqual(cache_thp._SESSIONS[i].last_usage, 0)
# Check that session data IS NOT in cache after cache.clear_all()
cache.clear_all()
for session in cache_thp._SESSIONS:
self.assertEqual(session.channel_id, b"")
self.assertEqual(session.session_id, b"")
self.assertEqual(session.last_usage, 0)
self.assertEqual(session.state, b"\x00")
def test_channel_capacity_in_cache(self):
self.assertTrue(cache_thp._MAX_CHANNELS_COUNT >= 3)
channels = []
for i in range(cache_thp._MAX_CHANNELS_COUNT):
channels.append(thp_common.get_new_channel(self.interface))
channel_ids = [channel.channel_cache.channel_id for channel in channels]
# Assert that each channel_id is unique and that cache and list of channels
# have the same "channels" on the same indexes
for i in range(len(channel_ids)):
self.assertEqual(cache_thp._CHANNELS[i].channel_id, channel_ids[i])
for j in range(i + 1, len(channel_ids)):
self.assertNotEqual(channel_ids[i], channel_ids[j])
# Create a new channel that is over the capacity
new_channel = thp_common.get_new_channel(self.interface)
for c in channels:
self.assertNotEqual(c.channel_id, new_channel.channel_id)
# Test that the oldest (least used) channel was replaced (_CHANNELS[0])
self.assertNotEqual(cache_thp._CHANNELS[0].channel_id, channel_ids[0])
self.assertEqual(cache_thp._CHANNELS[0].channel_id, new_channel.channel_id)
# Update the "last used" value of the second channel in cache (_CHANNELS[1]) and
# assert that it is not replaced when creating a new channel
cache_thp.update_channel_last_used(channel_ids[1])
new_new_channel = thp_common.get_new_channel(self.interface)
self.assertEqual(cache_thp._CHANNELS[1].channel_id, channel_ids[1])
# Assert that it was in fact the _CHANNEL[2] that was replaced
self.assertNotEqual(cache_thp._CHANNELS[2].channel_id, channel_ids[2])
self.assertEqual(
cache_thp._CHANNELS[2].channel_id, new_new_channel.channel_id
)
def test_session_capacity_in_cache(self):
self.assertTrue(cache_thp._MAX_SESSIONS_COUNT >= 4)
channel_cache_A = thp_common.get_new_channel(self.interface).channel_cache
channel_cache_B = thp_common.get_new_channel(self.interface).channel_cache
sesions_A = []
cid = []
sid = []
for i in range(3):
sesions_A.append(
cache_thp.create_or_replace_session(
channel_cache_A, (i + 1).to_bytes(1, "big")
)
)
cid.append(sesions_A[i].channel_id)
sid.append(sesions_A[i].session_id)
sessions_B = []
for i in range(cache_thp._MAX_SESSIONS_COUNT - 3):
sessions_B.append(
cache_thp.create_or_replace_session(
channel_cache_B, (i + 10).to_bytes(1, "big")
)
)
for i in range(3):
self.assertEqual(sesions_A[i], cache_thp._SESSIONS[i])
self.assertEqual(cid[i], cache_thp._SESSIONS[i].channel_id)
self.assertEqual(sid[i], cache_thp._SESSIONS[i].session_id)
for i in range(3, cache_thp._MAX_SESSIONS_COUNT):
self.assertEqual(sessions_B[i - 3], cache_thp._SESSIONS[i])
# Assert that new session replaces the oldest (least used) one (_SESSOIONS[0])
new_session = cache_thp.create_or_replace_session(channel_cache_B, b"\xab")
self.assertEqual(new_session, cache_thp._SESSIONS[0])
self.assertNotEqual(new_session.channel_id, cid[0])
self.assertNotEqual(new_session.session_id, sid[0])
# Assert that updating "last used" for session on channel A increases also
# the "last usage" of channel A.
self.assertTrue(channel_cache_A.last_usage < channel_cache_B.last_usage)
cache_thp.update_session_last_used(
channel_cache_A.channel_id, sesions_A[1].session_id
)
self.assertTrue(channel_cache_A.last_usage > channel_cache_B.last_usage)
new_new_session = cache_thp.create_or_replace_session(
channel_cache_B, b"\xaa"
)
# Assert that creating a new session on channel B shifts the "last usage" again
# and that _SESSIONS[1] was not replaced, but that _SESSIONS[2] was replaced
self.assertTrue(channel_cache_A.last_usage < channel_cache_B.last_usage)
self.assertEqual(sesions_A[1], cache_thp._SESSIONS[1])
self.assertNotEqual(sesions_A[2], cache_thp._SESSIONS[2])
self.assertEqual(new_new_session, cache_thp._SESSIONS[2])
def test_clear(self):
channel_A = thp_common.get_new_channel(self.interface)
channel_B = thp_common.get_new_channel(self.interface)
cid_A = channel_A.channel_id
cid_B = channel_B.channel_id
sessions = []
for i in range(3):
sessions.append(
cache_thp.create_or_replace_session(
channel_A.channel_cache, (i + 1).to_bytes(1, "big")
)
)
sessions.append(
cache_thp.create_or_replace_session(
channel_B.channel_cache, (i + 10).to_bytes(1, "big")
)
)
self.assertEqual(cache_thp._SESSIONS[2 * i].channel_id, cid_A)
self.assertNotEqual(cache_thp._SESSIONS[2 * i].last_usage, 0)
self.assertEqual(cache_thp._SESSIONS[2 * i + 1].channel_id, cid_B)
self.assertNotEqual(cache_thp._SESSIONS[2 * i + 1].last_usage, 0)
# Assert that clearing of channel A works
self.assertNotEqual(channel_A.channel_cache.channel_id, b"")
self.assertNotEqual(channel_A.channel_cache.last_usage, 0)
self.assertEqual(channel_A.get_channel_state(), ChannelState.TH1)
channel_A.clear()
self.assertEqual(channel_A.channel_cache.channel_id, b"")
self.assertEqual(channel_A.channel_cache.last_usage, 0)
self.assertEqual(channel_A.get_channel_state(), ChannelState.UNALLOCATED)
# Assert that clearing channel A also cleared all its sessions
for i in range(3):
self.assertEqual(cache_thp._SESSIONS[2 * i].last_usage, 0)
self.assertEqual(cache_thp._SESSIONS[2 * i].channel_id, b"")
self.assertNotEqual(cache_thp._SESSIONS[2 * i + 1].last_usage, 0)
self.assertEqual(cache_thp._SESSIONS[2 * i + 1].channel_id, cid_B)
cache.clear_all()
for session in cache_thp._SESSIONS:
self.assertEqual(session.last_usage, 0)
self.assertEqual(session.channel_id, b"")
for channel in cache_thp._CHANNELS:
self.assertEqual(channel.channel_id, b"")
self.assertEqual(channel.last_usage, 0)
self.assertEqual(
cache_thp._get_channel_state(channel), ChannelState.UNALLOCATED
)
def test_get_set(self):
channel = thp_common.get_new_channel(self.interface)
session_1 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x01"
)
session_1.set(KEY, b"hello")
self.assertEqual(session_1.get(KEY), b"hello")
session_2 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x02"
)
session_2.set(KEY, b"world")
self.assertEqual(session_2.get(KEY), b"world")
self.assertEqual(session_1.get(KEY), b"hello")
cache.clear_all()
self.assertIsNone(session_1.get(KEY))
self.assertIsNone(session_2.get(KEY))
def test_get_set_int(self):
channel = thp_common.get_new_channel(self.interface)
session_1 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x01"
)
session_1.set_int(KEY, 1234)
self.assertEqual(session_1.get_int(KEY), 1234)
session_2 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x02"
)
session_2.set_int(KEY, 5678)
self.assertEqual(session_2.get_int(KEY), 5678)
self.assertEqual(session_1.get_int(KEY), 1234)
cache.clear_all()
self.assertIsNone(session_1.get_int(KEY))
self.assertIsNone(session_2.get_int(KEY))
def test_get_set_bool(self):
channel = thp_common.get_new_channel(self.interface)
session_1 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x01"
)
with self.assertRaises(AssertionError):
session_1.set_bool(KEY, True)
# Change length of first session field to 0 so that the length check passes
session_1.fields = (0,) + session_1.fields[1:]
# with self.assertRaises(AssertionError) as e:
session_1.set_bool(KEY, True)
self.assertEqual(session_1.get_bool(KEY), True)
session_2 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x02"
)
session_2.fields = session_2.fields = (0,) + session_2.fields[1:]
session_2.set_bool(KEY, False)
self.assertEqual(session_2.get_bool(KEY), False)
self.assertEqual(session_1.get_bool(KEY), True)
cache.clear_all()
# Default value is False
self.assertFalse(session_1.get_bool(KEY))
self.assertFalse(session_2.get_bool(KEY))
def test_delete(self):
channel = thp_common.get_new_channel(self.interface)
session_1 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x01"
)
self.assertIsNone(session_1.get(KEY))
session_1.set(KEY, b"hello")
self.assertEqual(session_1.get(KEY), b"hello")
session_1.delete(KEY)
self.assertIsNone(session_1.get(KEY))
session_1.set(KEY, b"hello")
session_2 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x02"
)
self.assertIsNone(session_2.get(KEY))
session_2.set(KEY, b"hello")
self.assertEqual(session_2.get(KEY), b"hello")
session_2.delete(KEY)
self.assertIsNone(session_2.get(KEY))
self.assertEqual(session_1.get(KEY), b"hello")
else:
def setUpClass(self):
from trezor.wire import context
from trezor.wire.codec.codec_context import CodecContext
context.CURRENT_CONTEXT = CodecContext(None, bytearray(64))
def tearDownClass(self):
from trezor.wire import context
context.CURRENT_CONTEXT = None
def setUp(self):
cache.clear_all()
def test_start_session(self):
session_id_a = cache_codec.start_session()
self.assertIsNotNone(session_id_a)
session_id_b = cache_codec.start_session()
self.assertNotEqual(session_id_a, session_id_b)
cache.clear_all()
self.assertIsNone(get_active_session())
for session in cache_codec._SESSIONS:
self.assertEqual(session.session_id, b"")
self.assertEqual(session.last_usage, 0)
def test_end_session(self):
session_id = cache_codec.start_session()
self.assertTrue(is_session_started())
get_active_session().set(KEY, b"A")
cache_codec.end_current_session()
self.assertFalse(is_session_started())
self.assertIsNone(get_active_session())
# ending an ended session should be a no-op
cache_codec.end_current_session()
self.assertFalse(is_session_started())
session_id_a = cache_codec.start_session(session_id)
# original session no longer exists
self.assertNotEqual(session_id_a, session_id)
# original session data no longer exists
self.assertIsNone(get_active_session().get(KEY))
# create a new session
session_id_b = cache_codec.start_session()
# switch back to original session
session_id = cache_codec.start_session(session_id_a)
self.assertEqual(session_id, session_id_a)
# end original session
cache_codec.end_current_session()
# switch back to B
session_id = cache_codec.start_session(session_id_b)
self.assertEqual(session_id, session_id_b)
def test_session_queue(self):
session_id = cache_codec.start_session()
self.assertEqual(cache_codec.start_session(session_id), session_id)
get_active_session().set(KEY, b"A")
for _ in range(_PROTOCOL_CACHE._MAX_SESSIONS_COUNT):
cache_codec.start_session()
self.assertNotEqual(cache_codec.start_session(session_id), session_id)
self.assertIsNone(get_active_session().get(KEY))
def test_get_set(self):
session_id1 = cache_codec.start_session()
cache_codec.get_active_session().set(KEY, b"hello")
self.assertEqual(cache_codec.get_active_session().get(KEY), b"hello")
session_id2 = cache_codec.start_session()
cache_codec.get_active_session().set(KEY, b"world")
self.assertEqual(cache_codec.get_active_session().get(KEY), b"world")
cache_codec.start_session(session_id2)
self.assertEqual(cache_codec.get_active_session().get(KEY), b"world")
cache_codec.start_session(session_id1)
self.assertEqual(cache_codec.get_active_session().get(KEY), b"hello")
cache.clear_all()
self.assertIsNone(cache_codec.get_active_session())
def test_get_set_int(self):
session_id1 = cache_codec.start_session()
get_active_session().set_int(KEY, 1234)
self.assertEqual(get_active_session().get_int(KEY), 1234)
session_id2 = cache_codec.start_session()
get_active_session().set_int(KEY, 5678)
self.assertEqual(get_active_session().get_int(KEY), 5678)
cache_codec.start_session(session_id2)
self.assertEqual(get_active_session().get_int(KEY), 5678)
cache_codec.start_session(session_id1)
self.assertEqual(get_active_session().get_int(KEY), 1234)
cache.clear_all()
self.assertIsNone(get_active_session())
def test_delete(self):
session_id1 = cache_codec.start_session()
self.assertIsNone(get_active_session().get(KEY))
get_active_session().set(KEY, b"hello")
self.assertEqual(get_active_session().get(KEY), b"hello")
get_active_session().delete(KEY)
self.assertIsNone(get_active_session().get(KEY))
get_active_session().set(KEY, b"hello")
cache_codec.start_session() cache_codec.start_session()
self.assertNotEqual(cache_codec.start_session(session_id), session_id) self.assertIsNone(get_active_session().get(KEY))
self.assertIsNone(context.cache_get(KEY)) get_active_session().set(KEY, b"hello")
self.assertEqual(get_active_session().get(KEY), b"hello")
get_active_session().delete(KEY)
self.assertIsNone(get_active_session().get(KEY))
def test_get_set(self): cache_codec.start_session(session_id1)
session_id1 = cache_codec.start_session() self.assertEqual(get_active_session().get(KEY), b"hello")
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
session_id2 = cache_codec.start_session() def test_decorators(self):
context.cache_set(KEY, b"world") run_count = 0
self.assertEqual(context.cache_get(KEY), b"world") cache_codec.start_session()
from apps.common.cache import stored
cache_codec.start_session(session_id2) @stored(KEY)
self.assertEqual(context.cache_get(KEY), b"world") def func():
cache_codec.start_session(session_id1) nonlocal run_count
self.assertEqual(context.cache_get(KEY), b"hello") run_count += 1
return b"foo"
cache.clear_all() # cache is empty
with self.assertRaises(cache_common.InvalidSessionError): self.assertIsNone(get_active_session().get(KEY))
context.cache_get(KEY) self.assertEqual(run_count, 0)
self.assertEqual(func(), b"foo")
# function was run
self.assertEqual(run_count, 1)
self.assertEqual(get_active_session().get(KEY), b"foo")
# function does not run again but returns cached value
self.assertEqual(func(), b"foo")
self.assertEqual(run_count, 1)
def test_get_set_int(self): def test_empty_value(self):
session_id1 = cache_codec.start_session() cache_codec.start_session()
context.cache_set_int(KEY, 1234)
self.assertEqual(context.cache_get_int(KEY), 1234)
session_id2 = cache_codec.start_session() self.assertIsNone(get_active_session().get(KEY))
context.cache_set_int(KEY, 5678) get_active_session().set(KEY, b"")
self.assertEqual(context.cache_get_int(KEY), 5678) self.assertEqual(get_active_session().get(KEY), b"")
cache_codec.start_session(session_id2) get_active_session().delete(KEY)
self.assertEqual(context.cache_get_int(KEY), 5678) run_count = 0
cache_codec.start_session(session_id1)
self.assertEqual(context.cache_get_int(KEY), 1234)
cache.clear_all() from apps.common.cache import stored
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_get_int(KEY)
def test_delete(self): @stored(KEY)
session_id1 = cache_codec.start_session() def func():
self.assertIsNone(context.cache_get(KEY)) nonlocal run_count
context.cache_set(KEY, b"hello") run_count += 1
self.assertEqual(context.cache_get(KEY), b"hello") return b""
context.cache_delete(KEY)
self.assertIsNone(context.cache_get(KEY))
context.cache_set(KEY, b"hello") self.assertEqual(func(), b"")
cache_codec.start_session() # function gets called once
self.assertIsNone(context.cache_get(KEY)) self.assertEqual(run_count, 1)
context.cache_set(KEY, b"hello") self.assertEqual(func(), b"")
self.assertEqual(context.cache_get(KEY), b"hello") # function is not called for a second time
context.cache_delete(KEY) self.assertEqual(run_count, 1)
self.assertIsNone(context.cache_get(KEY))
cache_codec.start_session(session_id1) @mock_storage
self.assertEqual(context.cache_get(KEY), b"hello") def test_Initialize(self):
from apps.base import handle_Initialize
def test_decorators(self): def call_Initialize(**kwargs):
run_count = 0 msg = Initialize(**kwargs)
cache_codec.start_session() return await_result(handle_Initialize(msg))
@stored(KEY) # calling Initialize without an ID allocates a new one
def func(): session_id = cache_codec.start_session()
nonlocal run_count features = call_Initialize()
run_count += 1 self.assertNotEqual(session_id, features.session_id)
return b"foo"
# cache is empty # calling Initialize with the current ID does not allocate a new one
self.assertIsNone(context.cache_get(KEY)) features = call_Initialize(session_id=session_id)
self.assertEqual(run_count, 0) self.assertEqual(session_id, features.session_id)
self.assertEqual(func(), b"foo")
# function was run
self.assertEqual(run_count, 1)
self.assertEqual(context.cache_get(KEY), b"foo")
# function does not run again but returns cached value
self.assertEqual(func(), b"foo")
self.assertEqual(run_count, 1)
@stored_async(KEY) # store "hello"
async def async_func(): get_active_session().set(KEY, b"hello")
nonlocal run_count # check that it is cleared
run_count += 1 features = call_Initialize()
return b"bar" session_id = features.session_id
self.assertIsNone(get_active_session().get(KEY))
# store "hello" again
get_active_session().set(KEY, b"hello")
self.assertEqual(get_active_session().get(KEY), b"hello")
# cache is still full # supplying a different session ID starts a new session
self.assertEqual(await_result(async_func()), b"foo") call_Initialize(session_id=b"A" * _PROTOCOL_CACHE.SESSION_ID_LENGTH)
self.assertEqual(run_count, 1) self.assertIsNone(get_active_session().get(KEY))
cache_codec.start_session() # but resuming a session loads the previous one
self.assertEqual(await_result(async_func()), b"bar") call_Initialize(session_id=session_id)
self.assertEqual(run_count, 2) self.assertEqual(get_active_session().get(KEY), b"hello")
# awaitable is also run only once
self.assertEqual(await_result(async_func()), b"bar")
self.assertEqual(run_count, 2)
def test_empty_value(self): def test_EndSession(self):
cache_codec.start_session()
self.assertIsNone(context.cache_get(KEY)) self.assertIsNone(get_active_session())
context.cache_set(KEY, b"") cache_codec.start_session()
self.assertEqual(context.cache_get(KEY), b"") self.assertTrue(is_session_started())
self.assertIsNone(get_active_session().get(KEY))
context.cache_delete(KEY) await_result(handle_EndSession(EndSession()))
run_count = 0 self.assertFalse(is_session_started())
self.assertIsNone(cache_codec.get_active_session())
@stored(KEY)
def func():
nonlocal run_count
run_count += 1
return b""
self.assertEqual(func(), b"")
# function gets called once
self.assertEqual(run_count, 1)
self.assertEqual(func(), b"")
# function is not called for a second time
self.assertEqual(run_count, 1)
@mock_storage
def test_Initialize(self):
def call_Initialize(**kwargs):
msg = Initialize(**kwargs)
return await_result(handle_Initialize(msg))
# calling Initialize without an ID allocates a new one
session_id = cache_codec.start_session()
features = call_Initialize()
self.assertNotEqual(session_id, features.session_id)
# calling Initialize with the current ID does not allocate a new one
features = call_Initialize(session_id=session_id)
self.assertEqual(session_id, features.session_id)
# store "hello"
context.cache_set(KEY, b"hello")
# check that it is cleared
features = call_Initialize()
session_id = features.session_id
self.assertIsNone(context.cache_get(KEY))
# store "hello" again
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
# supplying a different session ID starts a new cache
call_Initialize(session_id=b"A" * cache_codec.SESSION_ID_LENGTH)
self.assertIsNone(context.cache_get(KEY))
# but resuming a session loads the previous one
call_Initialize(session_id=session_id)
self.assertEqual(context.cache_get(KEY), b"hello")
def test_EndSession(self):
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
cache_codec.start_session()
self.assertTrue(is_session_started())
self.assertIsNone(context.cache_get(KEY))
await_result(handle_EndSession(EndSession()))
self.assertFalse(is_session_started())
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,61 +3,11 @@ from common import * # isort:skip
import ustruct import ustruct
from mock_wire_interface import MockHID
from trezor import io from trezor import io
from trezor.loop import wait
from trezor.utils import chunks from trezor.utils import chunks
from trezor.wire.codec import codec_v1 from trezor.wire.codec import codec_v1
class MockHID:
TX_PACKET_LEN = 64
RX_PACKET_LEN = 64
def __init__(self, num):
self.num = num
self.data = []
self.packet = None
def pad_packet(self, data):
if len(data) > self.RX_PACKET_LEN:
raise Exception("Too long packet")
padding_length = self.RX_PACKET_LEN - len(data)
return data + b"\x00" * padding_length
def iface_num(self):
return self.num
def write(self, msg):
self.data.append(bytearray(msg))
return len(msg)
def mock_read(self, packet, gen):
self.packet = self.pad_packet(packet)
return gen.send(self.RX_PACKET_LEN)
def read(self, buffer, offset=0):
if self.packet is None:
raise Exception("No packet to read")
if offset > len(buffer):
raise Exception("Offset out of bounds")
buffer_space = len(buffer) - offset
if len(self.packet) > buffer_space:
raise Exception("Buffer too small")
else:
end = offset + len(self.packet)
buffer[offset:end] = self.packet
read = len(self.packet)
self.packet = None
return read
def wait_object(self, mode):
return wait(mode | self.num)
MESSAGE_TYPE = 0x4242 MESSAGE_TYPE = 0x4242
HEADER_PAYLOAD_LENGTH = MockHID.RX_PACKET_LEN - 3 - ustruct.calcsize(">HL") HEADER_PAYLOAD_LENGTH = MockHID.RX_PACKET_LEN - 3 - ustruct.calcsize(">HL")

View File

@ -0,0 +1,95 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
if utils.USE_THP:
from trezor.wire.thp import checksum
@unittest.skipUnless(utils.USE_THP, "only needed for THP")
class TestTrezorHostProtocolChecksum(unittest.TestCase):
vectors_correct = [
(
b"",
b"\x00\x00\x00\x00",
),
(
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"\x19\x0A\x55\xAD",
),
(
bytes("a", "ascii"),
b"\xE8\xB7\xBE\x43",
),
(
bytes("abc", "ascii"),
b"\x35\x24\x41\xC2",
),
(
bytes("123456789", "ascii"),
b"\xCB\xF4\x39\x26",
),
(
bytes(
"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
"ascii",
),
b"\x7C\xA9\x4A\x72",
),
(
b"\x76\x61\x72\x69\x6F\x75\x73\x20\x43\x52\x43\x20\x61\x6C\x67\x6F\x72\x69\x74\x68\x6D\x73\x20\x69\x6E\x70\x75\x74\x20\x64\x61\x74\x61",
b"\x9B\xD3\x66\xAE",
),
(
b"\x67\x3a\x5f\x0e\x39\xc0\x3c\x79\x58\x22\x74\x76\x64\x9e\x36\xe9\x0b\x04\x8c\xd2\xc0\x4d\x76\x63\x1a\xa2\x17\x85\xe8\x50\xa7\x14\x18\xfb\x86\xed\xa3\x59\x2d\x62\x62\x49\x64\x62\x26\x12\xdb\x95\x3d\xd6\xb5\xca\x4b\x22\x0d\xc5\x78\xb2\x12\x97\x8e\x54\x4e\x06\xb7\x9c\x90\xf5\xa0\x21\xa6\xc7\xd8\x39\xfd\xea\x3a\xf1\x7b\xa2\xe8\x71\x41\xd6\xcb\x1e\x5b\x0e\x29\xf7\x0c\xc7\x57\x8b\x53\x20\x1d\x2b\x41\x1c\x25\xf9\x07\xbb\xb4\x37\x79\x6a\x13\x1f\x6c\x43\x71\xc1\x1e\x70\xe6\x74\xd3\x9c\xbf\x32\x15\xee\xf2\xa7\x86\xbe\x59\x99\xc4\x10\x09\x8a\x6a\xaa\xd4\xd1\xd0\x71\xd2\x06\x1a\xdd\x2a\xa0\x08\xeb\x08\x6c\xfb\xd2\x2d\xfb\xaa\x72\x56\xeb\xd1\x92\x92\xe5\x0e\x95\x67\xf8\x38\xc3\xab\x59\x37\xe6\xfd\x42\xb0\xd0\x31\xd0\xcb\x8a\x66\xce\x2d\x53\x72\x1e\x72\xd3\x84\x25\xb0\xb8\x93\xd2\x61\x5b\x32\xd5\xe7\xe4\x0e\x31\x11\xaf\xdc\xb4\xb8\xee\xa4\x55\x16\x5f\x78\x86\x8b\x50\x4d\xc5\x6d\x6e\xfc\xe1\x6b\x06\x5b\x37\x84\x2a\x67\x95\x28\x00\xa4\xd1\x32\x9f\xbf\xe1\x64\xf8\x17\x47\xe1\xad\x8b\x72\xd2\xd9\x45\x5b\x73\x43\x3c\xe6\x21\xf7\x53\xa3\x73\xf9\x2a\xb0\xe9\x75\x5e\xa6\xbe\x9a\xad\xfc\xed\xb5\x46\x5b\x9f\xa9\x5a\x4f\xcb\xb6\x60\x96\x31\x91\x42\xca\xaf\xee\xa5\x0c\xe0\xab\x3e\x83\xb8\xac\x88\x10\x2c\x63\xd3\xc9\xd2\xf2\x44\xef\xea\x3d\x19\x24\x3c\x5b\xe7\x0c\x52\xfd\xfe\x47\x41\x14\xd5\x4c\x67\x8d\xdb\xe5\xd9\xfa\x67\x9c\x06\x31\x01\x92\xba\x96\xc4\x0d\xef\xf7\xc1\xe9\x23\x28\x0f\xae\x27\x9b\xff\x28\x0b\x3e\x85\x0c\xae\x02\xda\x27\xb6\x04\x51\x04\x43\x04\x99\x8c\xa3\x97\x1d\x84\xec\x55\x59\xfb\xf3\x84\xe5\xf8\x40\xf8\x5f\x81\x65\x92\x4c\x92\x7a\x07\x51\x8d\x6f\xff\x8d\x15\x36\x5c\x57\x7a\x5b\x3a\x63\x1c\x87\x65\xee\x54\xd5\x96\x50\x73\x1a\x9c\xff\x59\xe5\xea\x6f\x89\xd2\xbb\xa9\x6a\x12\x21\xf5\x08\x8e\x8a\xc0\xd8\xf5\x14\xe9\x9d\x7e\x99\x13\x88\x29\xa8\xb4\x22\x2a\x41\x7c\xc5\x10\xdf\x11\x5e\xf8\x8d\x0e\xd9\x98\xd5\xaf\xa8\xf9\x55\x1e\xe3\x29\xcd\x2c\x51\x7b\x8a\x8d\x52\xaa\x8b\x87\xae\x8e\xb2\xfa\x31\x27\x60\x90\xcb\x01\x6f\x7a\x79\x38\x04\x05\x7c\x11\x79\x10\x40\x33\x70\x75\xfd\x0b\x88\xa5\xcd\x35\xd8\xa6\x3b\xb0\x45\x82\x64\xd1\xb5\xdc\x06\xc9\x89\xf4\x16\x3e\xc7\xb3\xf1\x9d\xd3\xc5\xe3\xaf\xe8\x25\x86\x7a\x4a\xfd\x10\x5d\x20\xe5\x76\x5a\x22\x5f\x8f\xbc\xaa\x97\xee\xf2\xc2\x4c\x0e\xdc\x7b\xc4\xee\x53\xa3\xe0\xfa\xcd\x1e\x4e\x54\x1d\x5e\xe1\x51\x17\x1f\x1a\x75\x7f\xed\x12\xd7\xf7\xe3\x18\x56\x24\xcf\xc6\x96\x30\x77\x0d\x73\x98\x9c\x09\x69\xa3\xbc\x96\x5e\xaf\xde\x76\xa4\x66\x04\x6b\x36\x2a\xac\x6d\x37\xf8\x1e\xe1\x2a\x3e\x42\x2d\x1d\xe6\x46\xdd\x28\xb9\x08\x44\xa1\x9e\xb2\x22\x7a\x45\x8a\x37\x39\x74\xb4\xae\xc8\x3b\x40\xf7\xec\xbf\xfd\xe5\xde\xb2\x83\x5e\xa4\x46\x19\xa6\x9d\xb0\xe8\x76\x80\xbd\xc1\x80\x7a\xd9\xeb\xe7\x90\x5b\x81\x25\x21\xd9\x5b\x4a\x80\x48\x92\x71\x77\x04\xb2\xac\x05\xc9\xdf\x5e\x44\x5a\xae\x6e\xb3\xd8\x30\x5e\xdc\x77\x2f\x79\xc2\x8e\x8b\x28\x24\x06\x1b\x6f\x8d\x88\x53\x80\x55\x0c\x3a\x7b\x85\xb8\x96\x85\xe9\xf0\x57\x63\xfe\x32\x80\xff\x57\xc9\x3c\xdb\xf6\xcd\x67\x14\x47\x6c\x43\x3d\x6d\x48\x3f\x9c\x00\x60\x0e\xf5\x94\xe4\x52\x97\x86\xcd\xac\xbc\xe4\xe3\xe7\xee\xa2\x91\x6e\x92\xbb\xd1\x55\x0c\x5c\x0d\x63\xdb\x6b\xb8\x6e\x45\x48\x0f\xdf\x44\x48\xd2\xf5\xf7\x4d\x7b\xd4\x4d\xd3\xcd\xcd\x5b\x40\x60\xb1\xb2\x8e\xc9\x9a\x65\xc5\x06\x24\xcf\xe9\xcc\x5e\x2c\x49\x47\x38\x45\x5d\xc5\xc0\x0d\x8a\x07\x1c\xb3\xbb\xb1\x69\xf5\x6d\x0e\x9c\x96\x14\x93\x58\x0c\xc9\x48\x74\xfc\x35\xda\x7d\x4e\x32\x73\xa3\x77\x4a\x9e\xc5\xd1\x08\xfe\xa6\xa0\xf1\x66\x72\xea\xc7\xae\x21\x81\x0e\x8a\xba\x99\x06\x97\xfc\xc6\x2b\x69\x53\xc6\x67\xec\x5d\xa1\xfc\xa1\x3b\xdd\x2a\xd6\x8f\x31\xa7\x8d\xec\xfe\x0a\x3b\x6b\x39\x70\x70\x09\x72\x12\xbc\x84\x67\xca\xd2\x4a\x17\x33\x94\x45\x25\xc7\xfd\x1e\xa2\x4a\x9e\x27\x9d\xfb\x87\xea\xe4\xfd\xb0\x11\x06\x9d\x72\xb9\x1d\xea\x9b\x81\x2e\x6a\x36\x76\x62\xfa\xbe\x96\x67\x7d\x35\xdd\x5e\x5c\x4f\x41\x0d\xce\xdb\x13\xb0\x46\x89\x92\x45\x02\x39\x0f\xe6\xd1\x20\x96\x1c\x34\x00\x8c\xc9\xdf\xe3\xf0\xb6\x92\x3a\xda\x5c\x96\xd9\x0b\x7d\x57\xf5\x78\x11\xc0\xcf\xbf\xb0\x92\x3d\xe5\x6a\x67\x34\xce\xd9\x16\x08\xa0\x09\x42\x0b\x07\x13\x7c\x73\x0c\xc6\x50\x17\x42\xcf\xd9\x85\xd9\x23\x3c\xb1\x40\x40\x0f\x94\x20\xed\x2d\xbf\x10\x44\x6e\x64\x65\xe5\x1d\x5f\xec\x24\xd8\x4b\xe8\xc2\xfb\x06\x11\x24\x3f\xdf\x54\x2d\xe8\x4d\xc2\x1c\x27\x11\xb8\xb3\xd4",
b"\x6B\xA4\xEC\x92",
),
]
vectors_incorrect = [
(
b"",
b"\x00\x00\x00\x00\x00",
),
(
b"",
b"",
),
(
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"\x19\x0A\x55\xAE",
),
(
bytes("A", "ascii"),
b"\xE8\xB7\xBE\x43",
),
(
bytes("abc ", "ascii"),
b"\x35\x24\x41\xC2",
),
(
bytes("1234567890", "ascii"),
b"\xCB\xF4\x39\x26",
),
(
bytes(
"1234567890123456789012345678901234567890123456789012345678901234567890123456789",
"ascii",
),
b"\x7C\xA9\x4A\x72",
),
]
def test_computation(self):
for data, chksum in self.vectors_correct:
self.assertEqual(checksum.compute(data), chksum)
def test_validation_correct(self):
for data, chksum in self.vectors_correct:
self.assertTrue(checksum.is_valid(chksum, data))
def test_validation_incorrect(self):
for data, chksum in self.vectors_incorrect:
self.assertFalse(checksum.is_valid(chksum, data))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,157 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
from trezorcrypto import aesgcm, curve25519
import storage
if utils.USE_THP:
import thp_common
from trezor.wire.thp import crypto
from trezor.wire.thp.crypto import IV_1, IV_2, Handshake
def get_dummy_device_secret():
return b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
@unittest.skipUnless(utils.USE_THP, "only needed for THP")
class TestTrezorHostProtocolCrypto(unittest.TestCase):
if utils.USE_THP:
handshake = Handshake()
key_1 = b"\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07"
# 0:key, 1:nonce, 2:auth_data, 3:plaintext, 4:expected_ciphertext, 5:expected_tag
vectors_enc = [
(
key_1,
0,
b"\x55\x64",
b"\x00\x01\x02\x03\x04\05\x06\x07\x08\x09",
b"e2c9dd152fbee5821ea7",
b"10625812de81b14a46b9f1e5100a6d0c",
),
(
key_1,
1,
b"\x55\x64",
b"\x00\x01\x02\x03\x04\05\x06\x07\x08\x09",
b"79811619ddb07c2b99f8",
b"71c6b872cdc499a7e9a3c7441f053214",
),
(
key_1,
369,
b"\x55\x64",
b"\x00\x01\x02\x03\x04\05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
b"03bd030390f2dfe815a61c2b157a064f",
b"c1200f8a7ae9a6d32cef0fff878d55c2",
),
(
key_1,
369,
b"\x55\x64\x73\x82\x91",
b"\x00\x01\x02\x03\x04\05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
b"03bd030390f2dfe815a61c2b157a064f",
b"693ac160cd93a20f7fc255f049d808d0",
),
]
# 0:chaining key, 1:input, 2:output_1, 3:output:2
vectors_hkdf = [
(
crypto.PROTOCOL_NAME,
b"\x01\x02",
b"c784373a217d6be057cddc6068e6748f255fc8beb6f99b7b90cbc64aad947514",
b"12695451e29bf08ffe5e4e6ab734b0c3d7cdd99b16cd409f57bd4eaa874944ba",
),
(
b"\xc7\x84\x37\x3a\x21\x7d\x6b\xe0\x57\xcd\xdc\x60\x68\xe6\x74\x8f\x25\x5f\xc8\xbe\xb6\xf9\x9b\x7b\x90\xcb\xc6\x4a\xad\x94\x75\x14",
b"\x31\x41\x59\x26\x52\x12\x34\x56\x78\x89\x04\xaa",
b"f88c1e08d5c3bae8f6e4a3d3324c8cbc60a805603e399e69c4bf4eacb27c2f48",
b"5f0216bdb7110ee05372286974da8c9c8b96e2efa15b4af430755f462bd79a76",
),
]
vectors_iv = [
(0, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"),
(1, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"),
(7, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07"),
(1025, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x01"),
(4294967295, b"\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff"),
(0xFFFFFFFFFFFFFFFF, b"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff"),
]
def __init__(self):
if __debug__:
thp_common.suppres_debug_log()
super().__init__()
def setUp(self):
utils.DISABLE_ENCRYPTION = False
def test_encryption(self):
for v in self.vectors_enc:
buffer = bytearray(v[3])
tag = crypto.enc(buffer, v[0], v[1], v[2])
self.assertEqual(hexlify(buffer), v[4])
self.assertEqual(hexlify(tag), v[5])
self.assertTrue(crypto.dec(buffer, tag, v[0], v[1], v[2]))
self.assertEqual(buffer, v[3])
def test_hkdf(self):
for v in self.vectors_hkdf:
ck, k = crypto._hkdf(v[0], v[1])
self.assertEqual(hexlify(ck), v[2])
self.assertEqual(hexlify(k), v[3])
def test_iv_from_nonce(self):
for v in self.vectors_iv:
# x = v[0]
# y = x.to_bytes(8, "big")
iv = crypto._get_iv_from_nonce(v[0])
self.assertEqual(iv, v[1])
with self.assertRaises(AssertionError) as e:
iv = crypto._get_iv_from_nonce(0xFFFFFFFFFFFFFFFF + 1)
self.assertEqual(e.value.value, "Nonce overflow, terminate the channel")
def test_incorrect_vectors(self):
pass
def test_th1_crypto(self):
storage.device.get_device_secret = get_dummy_device_secret
handshake = self.handshake
host_ephemeral_privkey = curve25519.generate_secret()
host_ephemeral_pubkey = curve25519.publickey(host_ephemeral_privkey)
handshake.handle_th1_crypto(b"", host_ephemeral_pubkey)
def test_th2_crypto(self):
handshake = self.handshake
host_static_privkey = curve25519.generate_secret()
host_static_pubkey = curve25519.publickey(host_static_privkey)
aes_ctx = aesgcm(handshake.k, IV_2)
aes_ctx.auth(handshake.h)
encrypted_host_static_pubkey = bytearray(
aes_ctx.encrypt(host_static_pubkey) + aes_ctx.finish()
)
# Code to encrypt Host's noise encrypted payload correctly:
protomsg = bytearray(b"\x10\x02\x10\x03")
temp_k = handshake.k
temp_h = handshake.h
temp_h = crypto._hash_of_two(temp_h, encrypted_host_static_pubkey)
_, temp_k = crypto._hkdf(
handshake.ck,
curve25519.multiply(handshake.trezor_ephemeral_privkey, host_static_pubkey),
)
aes_ctx = aesgcm(temp_k, IV_1)
aes_ctx.encrypt_in_place(protomsg)
aes_ctx.auth(temp_h)
tag = aes_ctx.finish()
encrypted_payload = bytearray(protomsg + tag)
# end of encrypted payload generation
handshake.handle_th2_crypto(encrypted_host_static_pubkey, encrypted_payload)
self.assertEqual(encrypted_payload[:4], b"\x10\x02\x10\x03")
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,381 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
from mock_wire_interface import MockHID
from trezor import config, io, protobuf
from trezor.crypto.curve import curve25519
from trezor.enums import ThpMessageType
from trezor.wire.errors import UnexpectedMessage
from trezor.wire.protocol_common import Message
if utils.USE_THP:
from typing import TYPE_CHECKING
import thp_common
from storage import cache_thp
from storage.cache_common import (
CHANNEL_HANDSHAKE_HASH,
CHANNEL_KEY_RECEIVE,
CHANNEL_KEY_SEND,
CHANNEL_NONCE_RECEIVE,
CHANNEL_NONCE_SEND,
)
from trezor.crypto import elligator2
from trezor.enums import ThpPairingMethod
from trezor.messages import (
ThpCodeEntryChallenge,
ThpCodeEntryCpaceHostTag,
ThpCredentialRequest,
ThpEndRequest,
ThpPairingRequest,
)
from trezor.wire.thp import (
ChannelState,
checksum,
interface_manager,
memory_manager,
thp_main,
)
from trezor.wire.thp.crypto import Handshake
from trezor.wire.thp.pairing_context import PairingContext
from apps.thp import pairing
if TYPE_CHECKING:
from trezor.wire import WireInterface
def get_dummy_key() -> bytes:
return b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x01\x02\x03\x04\x05\x06\x07\x08\x09\x20\x01\x02\x03\x04\x05\x06\x07\x08\x09\x30\x31"
def dummy_encode_iface(iface: WireInterface):
return thp_common._MOCK_INTERFACE_HID
def send_channel_allocation_request(
interface: MockHID, nonce: bytes | None = None
) -> bytes:
if nonce is None or len(nonce) != 8:
nonce = b"\x00\x11\x22\x33\x44\x55\x66\x77"
header = b"\x40\xff\xff\x00\x0c"
chksum = checksum.compute(header + nonce)
cid_req = header + nonce + chksum
gen = thp_main.thp_main_loop(interface)
expected_channel_index = cache_thp._get_next_channel_index()
gen.send(None)
interface.mock_read(cid_req, gen)
gen.send(None)
model = bytes(utils.INTERNAL_MODEL, "big")
response_data = (
b"\x0a\x04" + model + "\x10\x00\x18\x00\x20\x02\x28\x02\x28\x03\x28\x04"
)
response_without_crc = (
b"\x41\xff\xff\x00\x20"
+ nonce
+ cache_thp._CHANNELS[expected_channel_index].channel_id
+ response_data
)
chkcsum = checksum.compute(response_without_crc)
expected_response = response_without_crc + chkcsum + b"\x00" * 27
return expected_response
def get_channel_id_from_response(channel_allocation_response: bytes) -> int:
return int.from_bytes(channel_allocation_response[13:15], "big")
def get_ack(channel_id: bytes) -> bytes:
if len(channel_id) != 2:
raise Exception("Channel id should by two bytes long")
return (
b"\x20"
+ channel_id
+ b"\x00\x04"
+ checksum.compute(b"\x20" + channel_id + b"\x00\x04")
+ b"\x00" * 55
)
@unittest.skipUnless(utils.USE_THP, "only needed for THP")
class TestTrezorHostProtocol(unittest.TestCase):
def __init__(self):
if __debug__:
thp_common.suppres_debug_log()
interface_manager.encode_iface = dummy_encode_iface
super().__init__()
def setUp(self):
self.interface = MockHID(0xDEADBEEF)
memory_manager.READ_BUFFER = bytearray(64)
memory_manager.WRITE_BUFFER = bytearray(256)
interface_manager.decode_iface = thp_common.dummy_decode_iface
def test_codec_message(self):
self.assertEqual(len(self.interface.data), 0)
gen = thp_main.thp_main_loop(self.interface)
gen.send(None)
# There should be a failiure response to received init packet (starts with "?##")
test_codec_message = b"?## Some data"
self.interface.mock_read(test_codec_message, gen)
gen.send(None)
self.assertEqual(len(self.interface.data), 1)
expected_response = b"?##\x00\x03\x00\x00\x00\x14\x08\x10"
self.assertEqual(
self.interface.data[-1][: len(expected_response)], expected_response
)
# There should be no response for continuation packet (starts with "?" only)
test_codec_message_2 = b"? Cont packet"
self.interface.mock_read(test_codec_message_2, gen)
# Check that sending None fails on AssertionError
with self.assertRaises(AssertionError):
gen.send(None)
self.assertEqual(len(self.interface.data), 1)
def test_message_on_unallocated_channel(self):
gen = thp_main.thp_main_loop(self.interface)
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
message_to_channel_789a = (
b"\x04\x78\x9a\x00\x0c\x00\x11\x22\x33\x44\x55\x66\x77\x96\x64\x3c\x6c"
)
self.interface.mock_read(message_to_channel_789a, gen)
gen.send(None)
unallocated_chanel_error_on_channel_789a = "42789a0005027b743563000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
self.assertEqual(
utils.get_bytes_as_str(self.interface.data[-1]),
unallocated_chanel_error_on_channel_789a,
)
def tbd_channel_allocation(self):
self.assertEqual(len(thp_main._CHANNELS), 0)
for c in cache_thp._CHANNELS:
self.assertEqual(int.from_bytes(c.state, "big"), ChannelState.UNALLOCATED)
expected_channel_index = cache_thp._get_next_channel_index()
expected_response = send_channel_allocation_request(self.interface)
self.assertEqual(self.interface.data[-1], expected_response)
cid = cache_thp._CHANNELS[expected_channel_index].channel_id
self.assertTrue(int.from_bytes(cid, "big") in thp_main._CHANNELS)
self.assertEqual(len(thp_main._CHANNELS), 1)
# test channel's default state is TH1:
cid = get_channel_id_from_response(self.interface.data[-1])
self.assertEqual(thp_main._CHANNELS[cid].get_channel_state(), ChannelState.TH1)
def tbd_invalid_encrypted_tag(self):
gen = thp_main.thp_main_loop(self.interface)
gen.send(None)
# prepare 2 new channels
expected_response_1 = send_channel_allocation_request(self.interface)
expected_response_2 = send_channel_allocation_request(self.interface)
self.assertEqual(self.interface.data[-2], expected_response_1)
self.assertEqual(self.interface.data[-1], expected_response_2)
# test invalid encryption tag
config.init()
config.wipe()
cid_1 = get_channel_id_from_response(expected_response_1)
channel = thp_main._CHANNELS[cid_1]
channel.iface = self.interface
channel.set_channel_state(ChannelState.ENCRYPTED_TRANSPORT)
header = b"\x04" + channel.channel_id + b"\x00\x14"
tag = b"\x00" * 16
chksum = checksum.compute(header + tag)
message_with_invalid_tag = header + tag + chksum
channel.channel_cache.set(CHANNEL_KEY_RECEIVE, get_dummy_key())
channel.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, 0)
cid_1_bytes = int.to_bytes(cid_1, 2, "big")
expected_ack_on_received_message = get_ack(cid_1_bytes)
self.interface.mock_read(message_with_invalid_tag, gen)
gen.send(None)
self.assertEqual(
self.interface.data[-1],
expected_ack_on_received_message,
)
error_without_crc = b"\x42" + cid_1_bytes + b"\x00\x05\x03"
chksum_err = checksum.compute(error_without_crc)
gen.send(None)
decryption_failed_error = error_without_crc + chksum_err + b"\x00" * 54
self.assertEqual(
self.interface.data[-1],
decryption_failed_error,
)
def tbd_test_channel_errors(self):
gen = thp_main.thp_main_loop(self.interface)
gen.send(None)
# prepare 2 new channels
expected_response_1 = send_channel_allocation_request(self.interface)
expected_response_2 = send_channel_allocation_request(self.interface)
self.assertEqual(self.interface.data[-2], expected_response_1)
self.assertEqual(self.interface.data[-1], expected_response_2)
# test invalid encryption tag
config.init()
config.wipe()
cid_1 = get_channel_id_from_response(expected_response_1)
channel = thp_main._CHANNELS[cid_1]
channel.iface = self.interface
channel.set_channel_state(ChannelState.ENCRYPTED_TRANSPORT)
header = b"\x04" + channel.channel_id + b"\x00\x14"
tag = b"\x00" * 16
chksum = checksum.compute(header + tag)
message_with_invalid_tag = header + tag + chksum
channel.channel_cache.set(CHANNEL_KEY_RECEIVE, get_dummy_key())
channel.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, 0)
cid_1_bytes = int.to_bytes(cid_1, 2, "big")
# expected_ack_on_received_message = get_ack(cid_1_bytes)
self.interface.mock_read(message_with_invalid_tag, gen)
# gen.send(None)
# self.assertEqual(
# self.interface.data[-1],
# expected_ack_on_received_message,
# )
error_without_crc = b"\x42" + cid_1_bytes + b"\x00\x05\x03"
chksum_err = checksum.compute(error_without_crc)
# gen.send(None)
decryption_failed_error = error_without_crc + chksum_err + b"\x00" * 54
self.assertEqual(
self.interface.data[-1],
decryption_failed_error,
)
# test invalid tag in handshake phase
cid_2 = get_channel_id_from_response(expected_response_1)
# cid_2_bytes = cid_2.to_bytes(2, "big")
channel = thp_main._CHANNELS[cid_2]
channel.iface = self.interface
channel.set_channel_state(ChannelState.TH2)
message_with_invalid_tag = b"\x0a\x12\x36\x00\x14\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77\x91\x65\x4c\xf9"
channel.channel_cache.set(CHANNEL_KEY_RECEIVE, get_dummy_key())
channel.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, 0)
# gen.send(message_with_invalid_tag)
# gen.send(None)
# gen.send(None)
# for i in self.interface.data:
# print(utils.get_bytes_as_str(i))
def tbd_skip_pairing(self):
config.init()
config.wipe()
channel = next(iter(thp_main._CHANNELS.values()))
channel.selected_pairing_methods = [
ThpPairingMethod.SkipPairing,
ThpPairingMethod.CodeEntry,
ThpPairingMethod.NFC_Unidirectional,
ThpPairingMethod.QrCode,
]
pairing_ctx = PairingContext(channel)
request_message = ThpPairingRequest()
channel.set_channel_state(ChannelState.TP1)
gen = pairing.handle_pairing_request(pairing_ctx, request_message)
with self.assertRaises(StopIteration):
gen.send(None)
self.assertEqual(channel.get_channel_state(), ChannelState.ENCRYPTED_TRANSPORT)
# Teardown: set back initial channel state value
channel.set_channel_state(ChannelState.TH1)
def TODO_test_pairing(self):
config.init()
config.wipe()
cid = get_channel_id_from_response(
send_channel_allocation_request(self.interface)
)
channel = thp_main._CHANNELS[cid]
channel.selected_pairing_methods = [
ThpPairingMethod.CodeEntry,
ThpPairingMethod.NFC,
ThpPairingMethod.QrCode,
]
pairing_ctx = PairingContext(channel)
request_message = ThpPairingRequest()
with self.assertRaises(UnexpectedMessage) as e:
pairing.handle_pairing_request(pairing_ctx, request_message)
print(e.value.message)
channel.set_channel_state(ChannelState.TP1)
gen = pairing.handle_pairing_request(pairing_ctx, request_message)
channel.channel_cache.set(CHANNEL_KEY_SEND, get_dummy_key())
channel.channel_cache.set_int(CHANNEL_NONCE_SEND, 0)
channel.channel_cache.set(CHANNEL_HANDSHAKE_HASH, b"")
gen.send(None)
async def _dummy(ctx: PairingContext, expected_types):
return await ctx.read([1018, 1024])
# pairing.show_display_data = _dummy
msg_code_entry = ThpCodeEntryChallenge(challenge=b"\x12\x34")
buffer: bytearray = bytearray(protobuf.encoded_length(msg_code_entry))
protobuf.encode(buffer, msg_code_entry)
code_entry_challenge = Message(ThpMessageType.ThpCodeEntryChallenge, buffer)
self.interface.mock_read(code_entry_challenge, gen)
# tag_qrc = b"\x55\xdf\x6c\xba\x0b\xe9\x5e\xd1\x4b\x78\x61\xec\xfa\x07\x9b\x5d\x37\x60\xd8\x79\x9c\xd7\x89\xb4\x22\xc1\x6f\x39\xde\x8f\x3b\xc3"
# tag_nfc = b"\x8f\xf0\xfa\x37\x0a\x5b\xdb\x29\x32\x21\xd8\x2f\x95\xdd\xb6\xb8\xee\xfd\x28\x6f\x56\x9f\xa9\x0b\x64\x8c\xfc\x62\x46\x5a\xdd\xd0"
pregenerator_host = b"\xf6\x94\xc3\x6f\xb3\xbd\xfb\xba\x2f\xfd\x0c\xd0\x71\xed\x54\x76\x73\x64\x37\xfa\x25\x85\x12\x8d\xcf\xb5\x6c\x02\xaf\x9d\xe8\xbe"
generator_host = elligator2.map_to_curve25519(pregenerator_host)
cpace_host_private_key = b"\x02\x80\x70\x3c\x06\x45\x19\x75\x87\x0c\x82\xe1\x64\x11\xc0\x18\x13\xb2\x29\x04\xb3\xf0\xe4\x1e\x6b\xfd\x77\x63\x11\x73\x07\xa9"
cpace_host_public_key: bytes = curve25519.multiply(
cpace_host_private_key, generator_host
)
msg = ThpCodeEntryCpaceHost(cpace_host_public_key=cpace_host_public_key)
# msg = ThpQrCodeTag(tag=tag_qrc)
# msg = ThpNfcTagHost(tag=tag_nfc)
buffer: bytearray = bytearray(protobuf.encoded_length(msg))
protobuf.encode(buffer, msg)
user_message = Message(ThpMessageType.ThpCodeEntryCpaceHost, buffer)
self.interface.mock_read(user_message, gen)
tag_ent = b"\xd0\x15\xd6\x72\x7c\xa6\x9b\x2a\x07\xfa\x30\xee\x03\xf0\x2d\x04\xdc\x96\x06\x77\x0c\xbd\xb4\xaa\x77\xc7\x68\x6f\xae\xa9\xdd\x81"
msg = ThpCodeEntryTag(tag=tag_ent)
buffer: bytearray = bytearray(protobuf.encoded_length(msg))
protobuf.encode(buffer, msg)
user_message = Message(ThpMessageType.ThpCodeEntryTag, buffer)
self.interface.mock_read(user_message, gen)
host_static_pubkey = b"\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77\x00\x11\x22\x33\x44\x55\x66\x77"
msg = ThpCredentialRequest(host_static_pubkey=host_static_pubkey)
buffer: bytearray = bytearray(protobuf.encoded_length(msg))
protobuf.encode(buffer, msg)
credential_request = Message(ThpMessageType.ThpCredentialRequest, buffer)
self.interface.mock_read(credential_request, gen)
msg = ThpEndRequest()
buffer: bytearray = bytearray(protobuf.encoded_length(msg))
protobuf.encode(buffer, msg)
end_request = Message(1012, buffer)
with self.assertRaises(StopIteration) as e:
self.interface.mock_read(end_request, gen)
print("response message:", e.value.value.MESSAGE_NAME)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,150 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
from typing import Any, Awaitable
if utils.USE_THP:
import thp_common
from mock_wire_interface import MockHID
from trezor.wire.thp import ENCRYPTED, PacketHeader, writer
@unittest.skipUnless(utils.USE_THP, "only needed for THP")
class TestTrezorHostProtocolWriter(unittest.TestCase):
short_payload_expected = b"04123400050700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
longer_payload_expected = [
b"0412340100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a",
b"8012343b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677",
b"80123478797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4",
b"801234b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1",
b"801234f2f3f4f5f6f7f8f9fafbfcfdfeff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
]
eight_longer_payloads_expected = [
b"0412340800000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a",
b"8012343b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677",
b"80123478797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4",
b"801234b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1",
b"801234f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e",
b"8012342f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b",
b"8012346c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8",
b"801234a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5",
b"801234e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122",
b"801234232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
b"801234606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c",
b"8012349d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9",
b"801234dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f10111213141516",
b"8012341718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253",
b"8012345455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90",
b"8012349192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccd",
b"801234cecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a",
b"8012340b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647",
b"80123448494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384",
b"80123485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1",
b"801234c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
b"801234ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b",
b"8012343c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778",
b"801234797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5",
b"801234b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2",
b"801234f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f",
b"801234303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c",
b"8012346d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9",
b"801234aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6",
b"801234e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
b"8012342425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60",
b"8012346162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d",
b"8012349e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9da",
b"801234dbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000000000000000000000000000000000000000000000000",
]
empty_payload_with_checksum_expected = b"0412340004edbd479c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
longer_payload_with_checksum_expected = [
b"0412340100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a",
b"8012343b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677",
b"80123478797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4",
b"801234b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1",
b"801234f2f3f4f5f6f7f8f9fafbfcfdfefff40c65ee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
]
def await_until_result(self, task: Awaitable) -> Any:
with self.assertRaises(StopIteration):
while True:
task.send(None)
def __init__(self):
if __debug__:
thp_common.suppres_debug_log()
super().__init__()
def setUp(self):
self.interface = MockHID(0xDEADBEEF)
def test_write_empty_packet(self):
self.await_until_result(writer.write_packet_to_wire(self.interface, b""))
print(self.interface.data[0])
self.assertEqual(len(self.interface.data), 1)
self.assertEqual(self.interface.data[0], b"")
def test_write_empty_payload(self):
header = PacketHeader(ENCRYPTED, 4660, 4)
await_result(writer.write_payloads_to_wire(self.interface, header, (b"",)))
self.assertEqual(len(self.interface.data), 0)
def test_write_short_payload(self):
header = PacketHeader(ENCRYPTED, 4660, 5)
data = b"\x07"
self.await_until_result(
writer.write_payloads_to_wire(self.interface, header, (data,))
)
self.assertEqual(hexlify(self.interface.data[0]), self.short_payload_expected)
def test_write_longer_payload(self):
data = bytearray(range(256))
header = PacketHeader(ENCRYPTED, 4660, 256)
self.await_until_result(
writer.write_payloads_to_wire(self.interface, header, (data,))
)
for i in range(len(self.longer_payload_expected)):
self.assertEqual(
hexlify(self.interface.data[i]), self.longer_payload_expected[i]
)
def test_write_eight_longer_payloads(self):
data = bytearray(range(256))
header = PacketHeader(ENCRYPTED, 4660, 2048)
self.await_until_result(
writer.write_payloads_to_wire(
self.interface, header, (data, data, data, data, data, data, data, data)
)
)
for i in range(len(self.eight_longer_payloads_expected)):
self.assertEqual(
hexlify(self.interface.data[i]), self.eight_longer_payloads_expected[i]
)
def test_write_empty_payload_with_checksum(self):
header = PacketHeader(ENCRYPTED, 4660, 4)
self.await_until_result(
writer.write_payload_to_wire_and_add_checksum(self.interface, header, b"")
)
self.assertEqual(
hexlify(self.interface.data[0]), self.empty_payload_with_checksum_expected
)
def test_write_longer_payload_with_checksum(self):
data = bytearray(range(256))
header = PacketHeader(ENCRYPTED, 4660, 256)
self.await_until_result(
writer.write_payload_to_wire_and_add_checksum(self.interface, header, data)
)
for i in range(len(self.longer_payload_with_checksum_expected)):
self.assertEqual(
hexlify(self.interface.data[i]),
self.longer_payload_with_checksum_expected[i],
)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,337 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
import ustruct
from typing import TYPE_CHECKING
from mock_wire_interface import MockHID
from storage.cache_thp import BROADCAST_CHANNEL_ID
from trezor import io
from trezor.utils import chunks
from trezor.wire.protocol_common import Message
if utils.USE_THP:
import thp_common
import trezor.wire.thp
from trezor.wire.thp import alternating_bit_protocol as ABP
from trezor.wire.thp import checksum, thp_main
from trezor.wire.thp.checksum import CHECKSUM_LENGTH
if TYPE_CHECKING:
from trezorio import WireInterface
MESSAGE_TYPE = 0x4242
MESSAGE_TYPE_BYTES = b"\x42\x42"
_MESSAGE_TYPE_LEN = 2
PLAINTEXT_0 = 0x01
PLAINTEXT_1 = 0x11
COMMON_CID = 4660
CONT = 0x80
HEADER_INIT_LENGTH = 5
HEADER_CONT_LENGTH = 3
if utils.USE_THP:
INIT_MESSAGE_DATA_LENGTH = HEADER_INIT_LENGTH - _MESSAGE_TYPE_LEN # + PACKET_LENGTH
def make_header(ctrl_byte, cid, length):
return ustruct.pack(">BHH", ctrl_byte, cid, length)
def make_cont_header():
return ustruct.pack(">BH", CONT, COMMON_CID)
def makeSimpleMessage(header, message_type, message_data):
return header + ustruct.pack(">H", message_type) + message_data
def makeCidRequest(header, message_data):
return header + message_data
def getPlaintext() -> bytes:
if ABP.get_expected_receive_seq_bit(THP.get_active_session()) == 1:
return PLAINTEXT_1
return PLAINTEXT_0
async def deprecated_read_message(
iface: WireInterface, buffer: utils.BufferType
) -> Message:
return Message(-1, b"\x00")
async def deprecated_write_message(
iface: WireInterface, message: Message, is_retransmission: bool = False
) -> None:
pass
# This test suite is an adaptation of test_trezor.wire.codec_v1
@unittest.skipUnless(utils.USE_THP, "only needed for THP")
class TestWireTrezorHostProtocolV1(unittest.TestCase):
def __init__(self):
if __debug__:
thp_common.suppres_debug_log()
super().__init__()
def setUp(self):
self.interface = MockHID(0xDEADBEEF)
def _simple(self):
cid_req_header = make_header(
ctrl_byte=0x40, cid=BROADCAST_CHANNEL_ID, length=12
)
cid_request_dummy_data = b"\x00\x11\x22\x33\x44\x55\x66\x77\x96\x64\x3c\x6c"
cid_req_message = makeCidRequest(cid_req_header, cid_request_dummy_data)
message_header = make_header(ctrl_byte=0x01, cid=COMMON_CID, length=18)
cid_request_dummy_data_checksum = b"\x67\x8e\xac\xe0"
message = makeSimpleMessage(
message_header,
MESSAGE_TYPE,
cid_request_dummy_data + cid_request_dummy_data_checksum,
)
buffer = bytearray(64)
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
gen.send(cid_req_message)
gen.send(None)
gen.send(message)
with self.assertRaises(StopIteration) as e:
gen.send(None)
# e.value is StopIteration. e.value.value is the return value of the call
result = e.value.value
self.assertEqual(result.type, MESSAGE_TYPE)
self.assertEqual(result.data, cid_request_dummy_data)
buffer_without_zeroes = buffer[: len(message) - 5]
message_without_header = message[5:]
# message should have been read into the buffer
self.assertEqual(buffer_without_zeroes, message_without_header)
def _read_one_packet(self):
# zero length message - just a header
PLAINTEXT = getPlaintext()
header = make_header(
PLAINTEXT, cid=COMMON_CID, length=_MESSAGE_TYPE_LEN + CHECKSUM_LENGTH
)
chksum = checksum.compute(header + MESSAGE_TYPE_BYTES)
message = header + MESSAGE_TYPE_BYTES + chksum
buffer = bytearray(64)
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
gen.send(message)
with self.assertRaises(StopIteration) as e:
gen.send(None)
# e.value is StopIteration. e.value.value is the return value of the call
result = e.value.value
self.assertEqual(result.type, MESSAGE_TYPE)
self.assertEqual(result.data, b"")
# message should have been read into the buffer
self.assertEqual(buffer, MESSAGE_TYPE_BYTES + chksum + b"\x00" * 58)
def _read_many_packets(self):
message = bytes(range(256))
header = make_header(
getPlaintext(),
COMMON_CID,
len(message) + _MESSAGE_TYPE_LEN + CHECKSUM_LENGTH,
)
chksum = checksum.compute(header + MESSAGE_TYPE_BYTES + message)
# message = MESSAGE_TYPE_BYTES + message + checksum
# first packet is init header + 59 bytes of data
# other packets are cont header + 61 bytes of data
cont_header = make_cont_header()
packets = [header + MESSAGE_TYPE_BYTES + message[:INIT_MESSAGE_DATA_LENGTH]] + [
cont_header + chunk
for chunk in chunks(
message[INIT_MESSAGE_DATA_LENGTH:] + chksum,
64 - HEADER_CONT_LENGTH,
)
]
buffer = bytearray(262)
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
for packet in packets:
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
query = gen.send(packet)
# last packet will stop
with self.assertRaises(StopIteration) as e:
gen.send(None)
# e.value is StopIteration. e.value.value is the return value of the call
result = e.value.value
self.assertEqual(result.type, MESSAGE_TYPE)
self.assertEqual(result.data, message)
# message should have been read into the buffer )
self.assertEqual(buffer, MESSAGE_TYPE_BYTES + message + chksum)
def _read_large_message(self):
message = b"hello world"
header = make_header(
getPlaintext(),
COMMON_CID,
_MESSAGE_TYPE_LEN + len(message) + CHECKSUM_LENGTH,
)
packet = (
header
+ MESSAGE_TYPE_BYTES
+ message
+ checksum.compute(header + MESSAGE_TYPE_BYTES + message)
)
# make sure we fit into one packet, to make this easier
# self.assertTrue(len(packet) <= thp_main.PACKET_LENGTH)
buffer = bytearray(1)
self.assertTrue(len(buffer) <= len(packet))
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
gen.send(packet)
with self.assertRaises(StopIteration) as e:
gen.send(None)
# e.value is StopIteration. e.value.value is the return value of the call
result = e.value.value
self.assertEqual(result.type, MESSAGE_TYPE)
self.assertEqual(result.data, message)
# read should have allocated its own buffer and not touch ours
self.assertEqual(buffer, b"\x00")
def _roundtrip(self):
message_payload = bytes(range(256))
message = Message(
MESSAGE_TYPE, message_payload, 1
) # TODO use different session id
gen = deprecated_write_message(self.interface, message)
# exhaust the iterator:
# (XXX we can only do this because the iterator is only accepting None and returns None)
for query in gen:
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_WRITE))
buffer = bytearray(1024)
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
for packet in self.interface.data:
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
print(utils.get_bytes_as_str(packet))
query = gen.send(packet)
with self.assertRaises(StopIteration) as e:
gen.send(None)
result = e.value.value
self.assertEqual(result.type, MESSAGE_TYPE)
self.assertEqual(result.data, message.data)
def _write_one_packet(self):
message = Message(MESSAGE_TYPE, b"")
gen = deprecated_write_message(self.interface, message)
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_WRITE))
with self.assertRaises(StopIteration):
gen.send(None)
header = make_header(
getPlaintext(), COMMON_CID, _MESSAGE_TYPE_LEN + CHECKSUM_LENGTH
)
expected_message = (
header
+ MESSAGE_TYPE_BYTES
+ checksum.compute(header + MESSAGE_TYPE_BYTES)
+ b"\x00" * (INIT_MESSAGE_DATA_LENGTH - CHECKSUM_LENGTH)
)
self.assertTrue(self.interface.data == [expected_message])
def _write_multiple_packets(self):
message_payload = bytes(range(256))
message = Message(MESSAGE_TYPE, message_payload)
gen = deprecated_write_message(self.interface, message)
header = make_header(
PLAINTEXT_1,
COMMON_CID,
len(message.data) + _MESSAGE_TYPE_LEN + CHECKSUM_LENGTH,
)
cont_header = make_cont_header()
chksum = checksum.compute(
header + message.type.to_bytes(2, "big") + message.data
)
packets = [
header + MESSAGE_TYPE_BYTES + message.data[:INIT_MESSAGE_DATA_LENGTH]
] + [
cont_header + chunk
for chunk in chunks(
message.data[INIT_MESSAGE_DATA_LENGTH:] + chksum,
HEADER_CONT_LENGTH, # + PACKET_LENGTH
)
]
for _ in packets:
# we receive as many queries as there are packets
query = gen.send(None)
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_WRITE))
# the first sent None only started the generator. the len(packets)-th None
# will finish writing and raise StopIteration
with self.assertRaises(StopIteration):
gen.send(None)
# packets must be identical up to the last one
self.assertListEqual(packets[:-1], self.interface.data[:-1])
# last packet must be identical up to message length. remaining bytes in
# the 64-byte packets are garbage -- in particular, it's the bytes of the
# previous packet
last_packet = packets[-1] + packets[-2][len(packets[-1]) :]
self.assertEqual(last_packet, self.interface.data[-1])
def _read_huge_packet(self):
PACKET_COUNT = 1180
# message that takes up 1 180 USB packets
message_size = (PACKET_COUNT - 1) * (
HEADER_CONT_LENGTH - CHECKSUM_LENGTH - _MESSAGE_TYPE_LEN # + PACKET_LENGTH
) + INIT_MESSAGE_DATA_LENGTH
# ensure that a message this big won't fit into memory
# Note: this control is changed, because THP has only 2 byte length field
self.assertTrue(message_size > thp_main.MAX_PAYLOAD_LEN)
# self.assertRaises(MemoryError, bytearray, message_size)
header = make_header(PLAINTEXT_1, COMMON_CID, message_size)
packet = header + MESSAGE_TYPE_BYTES + (b"\x00" * INIT_MESSAGE_DATA_LENGTH)
buffer = bytearray(65536)
gen = deprecated_read_message(self.interface, buffer)
query = gen.send(None)
# THP returns "Message too large" error after reading the message size,
# it is different from codec_v1 as it does not allow big enough messages
# to raise MemoryError in this test
self.assertObjectEqual(query, self.interface.wait_object(io.POLL_READ))
with self.assertRaises(trezor.wire.thp.ThpError) as e:
query = gen.send(packet)
self.assertEqual(e.value.args[0], "Message too large")
if __name__ == "__main__":
unittest.main()

47
core/tests/thp_common.py Normal file
View File

@ -0,0 +1,47 @@
# flake8: noqa: F403,F405
from trezor import utils
from trezor.wire.thp import ChannelState
if utils.USE_THP:
import unittest
from typing import TYPE_CHECKING
from mock_wire_interface import MockHID
from storage import cache_thp
from trezor.wire import context
from trezor.wire.thp import interface_manager
from trezor.wire.thp.channel import Channel
from trezor.wire.thp.session_context import SessionContext
_MOCK_INTERFACE_HID = b"\x00"
if TYPE_CHECKING:
from trezor.wire import WireInterface
def dummy_decode_iface(cached_iface: bytes):
return MockHID(0xDEADBEEF)
def get_new_channel(channel_iface: WireInterface | None = None) -> Channel:
interface_manager.decode_iface = dummy_decode_iface
channel_cache = cache_thp.get_new_channel(_MOCK_INTERFACE_HID)
channel = Channel(channel_cache)
channel.set_channel_state(ChannelState.TH1)
if channel_iface is not None:
channel.iface = channel_iface
return channel
def prepare_context() -> None:
channel = get_new_channel()
session_cache = cache_thp.create_or_replace_session(
channel.channel_cache, session_id=b"\x01"
)
session_ctx = SessionContext(channel, session_cache)
context.CURRENT_CONTEXT = session_ctx
if __debug__:
# Disable log.debug
def suppres_debug_log() -> None:
from trezor import log
log.debug = lambda name, msg, *args: None

View File

@ -191,6 +191,18 @@ void fsm_sendFailure(FailureType code, const char *text)
case FailureType_Failure_InvalidSession: case FailureType_Failure_InvalidSession:
text = _("Invalid session"); text = _("Invalid session");
break; break;
case FailureType_Failure_ThpUnallocatedSession:
text = _("Unallocated session");
break;
case FailureType_Failure_InvalidProtocol:
text = _("Invalid protocol");
break;
case FailureType_Failure_BufferError:
text = _("Buffer error");
break;
case FailureType_Failure_DeviceIsBusy:
text = _("Device is busy");
break;
case FailureType_Failure_FirmwareError: case FailureType_Failure_FirmwareError:
text = _("Firmware error"); text = _("Firmware error");
break; break;

View File

@ -3,14 +3,14 @@ Q := @
endif endif
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn \ SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn \
DebugLinkRecordScreen DebugLinkEraseSdCard DebugLinkWatchLayout \ DebugLinkRecordScreen DebugLinkEraseSdCard DebugLinkWatchLayout DebugLinkLayout \
DebugLinkLayout DebugLinkResetDebugEvents GetNonce \ DebugLinkResetDebugEvents DebugLinkGetPairingInfo DebugLinkPairingInfo GetNonce \
TxAckInput TxAckOutput TxAckPrev TxAckPaymentRequest \ TxAckInput TxAckOutput TxAckPrev TxAckPaymentRequest \
EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \ EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \
EthereumTypedDataValueRequest EthereumTypedDataValueAck ShowDeviceTutorial \ EthereumTypedDataValueRequest EthereumTypedDataValueAck ShowDeviceTutorial \
UnlockBootloader AuthenticateDevice AuthenticityProof \ UnlockBootloader AuthenticateDevice AuthenticityProof \
Solana StellarClaimClaimableBalanceOp \ Solana StellarClaimClaimableBalanceOp \
ChangeLanguage TranslationDataRequest TranslationDataAck \ ChangeLanguage TranslationDataRequest TranslationDataAck Thp\
SetBrightness DebugLinkOptigaSetSecMax EntropyCheckReady EntropyCheckContinue \ SetBrightness DebugLinkOptigaSetSecMax EntropyCheckReady EntropyCheckContinue \
BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult

View File

@ -0,0 +1 @@
../../vendor/trezor-common/protob/messages-thp.proto

View File

@ -8,3 +8,5 @@ libusb1>=1.6.4
construct>=2.9,!=2.10.55 construct>=2.9,!=2.10.55
typing_extensions>=4.7.1 typing_extensions>=4.7.1
construct-classes>=0.1.2 construct-classes>=0.1.2
cryptography >=43.0.3
platformdirs >=2

View File

@ -82,6 +82,8 @@ trezor_message_impl! {
DebugLinkWatchLayout => MessageType_DebugLinkWatchLayout, DebugLinkWatchLayout => MessageType_DebugLinkWatchLayout,
DebugLinkResetDebugEvents => MessageType_DebugLinkResetDebugEvents, DebugLinkResetDebugEvents => MessageType_DebugLinkResetDebugEvents,
DebugLinkOptigaSetSecMax => MessageType_DebugLinkOptigaSetSecMax, DebugLinkOptigaSetSecMax => MessageType_DebugLinkOptigaSetSecMax,
DebugLinkGetPairingInfo => MessageType_DebugLinkGetPairingInfo,
DebugLinkPairingInfo => MessageType_DebugLinkPairingInfo,
BenchmarkListNames => MessageType_BenchmarkListNames, BenchmarkListNames => MessageType_BenchmarkListNames,
BenchmarkNames => MessageType_BenchmarkNames, BenchmarkNames => MessageType_BenchmarkNames,
BenchmarkRun => MessageType_BenchmarkRun, BenchmarkRun => MessageType_BenchmarkRun,

View File

@ -226,6 +226,10 @@ pub enum MessageType {
MessageType_DebugLinkResetDebugEvents = 9007, MessageType_DebugLinkResetDebugEvents = 9007,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_DebugLinkOptigaSetSecMax) // @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_DebugLinkOptigaSetSecMax)
MessageType_DebugLinkOptigaSetSecMax = 9008, MessageType_DebugLinkOptigaSetSecMax = 9008,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_DebugLinkGetPairingInfo)
MessageType_DebugLinkGetPairingInfo = 9009,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_DebugLinkPairingInfo)
MessageType_DebugLinkPairingInfo = 9010,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_EthereumGetPublicKey) // @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_EthereumGetPublicKey)
MessageType_EthereumGetPublicKey = 450, MessageType_EthereumGetPublicKey = 450,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_EthereumPublicKey) // @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_EthereumPublicKey)
@ -632,6 +636,8 @@ impl ::protobuf::Enum for MessageType {
9006 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkWatchLayout), 9006 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkWatchLayout),
9007 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkResetDebugEvents), 9007 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkResetDebugEvents),
9008 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkOptigaSetSecMax), 9008 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkOptigaSetSecMax),
9009 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkGetPairingInfo),
9010 => ::std::option::Option::Some(MessageType::MessageType_DebugLinkPairingInfo),
450 => ::std::option::Option::Some(MessageType::MessageType_EthereumGetPublicKey), 450 => ::std::option::Option::Some(MessageType::MessageType_EthereumGetPublicKey),
451 => ::std::option::Option::Some(MessageType::MessageType_EthereumPublicKey), 451 => ::std::option::Option::Some(MessageType::MessageType_EthereumPublicKey),
56 => ::std::option::Option::Some(MessageType::MessageType_EthereumGetAddress), 56 => ::std::option::Option::Some(MessageType::MessageType_EthereumGetAddress),
@ -885,6 +891,8 @@ impl ::protobuf::Enum for MessageType {
"MessageType_DebugLinkWatchLayout" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkWatchLayout), "MessageType_DebugLinkWatchLayout" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkWatchLayout),
"MessageType_DebugLinkResetDebugEvents" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkResetDebugEvents), "MessageType_DebugLinkResetDebugEvents" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkResetDebugEvents),
"MessageType_DebugLinkOptigaSetSecMax" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkOptigaSetSecMax), "MessageType_DebugLinkOptigaSetSecMax" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkOptigaSetSecMax),
"MessageType_DebugLinkGetPairingInfo" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkGetPairingInfo),
"MessageType_DebugLinkPairingInfo" => ::std::option::Option::Some(MessageType::MessageType_DebugLinkPairingInfo),
"MessageType_EthereumGetPublicKey" => ::std::option::Option::Some(MessageType::MessageType_EthereumGetPublicKey), "MessageType_EthereumGetPublicKey" => ::std::option::Option::Some(MessageType::MessageType_EthereumGetPublicKey),
"MessageType_EthereumPublicKey" => ::std::option::Option::Some(MessageType::MessageType_EthereumPublicKey), "MessageType_EthereumPublicKey" => ::std::option::Option::Some(MessageType::MessageType_EthereumPublicKey),
"MessageType_EthereumGetAddress" => ::std::option::Option::Some(MessageType::MessageType_EthereumGetAddress), "MessageType_EthereumGetAddress" => ::std::option::Option::Some(MessageType::MessageType_EthereumGetAddress),
@ -1137,6 +1145,8 @@ impl ::protobuf::Enum for MessageType {
MessageType::MessageType_DebugLinkWatchLayout, MessageType::MessageType_DebugLinkWatchLayout,
MessageType::MessageType_DebugLinkResetDebugEvents, MessageType::MessageType_DebugLinkResetDebugEvents,
MessageType::MessageType_DebugLinkOptigaSetSecMax, MessageType::MessageType_DebugLinkOptigaSetSecMax,
MessageType::MessageType_DebugLinkGetPairingInfo,
MessageType::MessageType_DebugLinkPairingInfo,
MessageType::MessageType_EthereumGetPublicKey, MessageType::MessageType_EthereumGetPublicKey,
MessageType::MessageType_EthereumPublicKey, MessageType::MessageType_EthereumPublicKey,
MessageType::MessageType_EthereumGetAddress, MessageType::MessageType_EthereumGetAddress,
@ -1395,154 +1405,156 @@ impl ::protobuf::EnumFull for MessageType {
MessageType::MessageType_DebugLinkWatchLayout => 96, MessageType::MessageType_DebugLinkWatchLayout => 96,
MessageType::MessageType_DebugLinkResetDebugEvents => 97, MessageType::MessageType_DebugLinkResetDebugEvents => 97,
MessageType::MessageType_DebugLinkOptigaSetSecMax => 98, MessageType::MessageType_DebugLinkOptigaSetSecMax => 98,
MessageType::MessageType_EthereumGetPublicKey => 99, MessageType::MessageType_DebugLinkGetPairingInfo => 99,
MessageType::MessageType_EthereumPublicKey => 100, MessageType::MessageType_DebugLinkPairingInfo => 100,
MessageType::MessageType_EthereumGetAddress => 101, MessageType::MessageType_EthereumGetPublicKey => 101,
MessageType::MessageType_EthereumAddress => 102, MessageType::MessageType_EthereumPublicKey => 102,
MessageType::MessageType_EthereumSignTx => 103, MessageType::MessageType_EthereumGetAddress => 103,
MessageType::MessageType_EthereumSignTxEIP1559 => 104, MessageType::MessageType_EthereumAddress => 104,
MessageType::MessageType_EthereumTxRequest => 105, MessageType::MessageType_EthereumSignTx => 105,
MessageType::MessageType_EthereumTxAck => 106, MessageType::MessageType_EthereumSignTxEIP1559 => 106,
MessageType::MessageType_EthereumSignMessage => 107, MessageType::MessageType_EthereumTxRequest => 107,
MessageType::MessageType_EthereumVerifyMessage => 108, MessageType::MessageType_EthereumTxAck => 108,
MessageType::MessageType_EthereumMessageSignature => 109, MessageType::MessageType_EthereumSignMessage => 109,
MessageType::MessageType_EthereumSignTypedData => 110, MessageType::MessageType_EthereumVerifyMessage => 110,
MessageType::MessageType_EthereumTypedDataStructRequest => 111, MessageType::MessageType_EthereumMessageSignature => 111,
MessageType::MessageType_EthereumTypedDataStructAck => 112, MessageType::MessageType_EthereumSignTypedData => 112,
MessageType::MessageType_EthereumTypedDataValueRequest => 113, MessageType::MessageType_EthereumTypedDataStructRequest => 113,
MessageType::MessageType_EthereumTypedDataValueAck => 114, MessageType::MessageType_EthereumTypedDataStructAck => 114,
MessageType::MessageType_EthereumTypedDataSignature => 115, MessageType::MessageType_EthereumTypedDataValueRequest => 115,
MessageType::MessageType_EthereumSignTypedHash => 116, MessageType::MessageType_EthereumTypedDataValueAck => 116,
MessageType::MessageType_NEMGetAddress => 117, MessageType::MessageType_EthereumTypedDataSignature => 117,
MessageType::MessageType_NEMAddress => 118, MessageType::MessageType_EthereumSignTypedHash => 118,
MessageType::MessageType_NEMSignTx => 119, MessageType::MessageType_NEMGetAddress => 119,
MessageType::MessageType_NEMSignedTx => 120, MessageType::MessageType_NEMAddress => 120,
MessageType::MessageType_NEMDecryptMessage => 121, MessageType::MessageType_NEMSignTx => 121,
MessageType::MessageType_NEMDecryptedMessage => 122, MessageType::MessageType_NEMSignedTx => 122,
MessageType::MessageType_TezosGetAddress => 123, MessageType::MessageType_NEMDecryptMessage => 123,
MessageType::MessageType_TezosAddress => 124, MessageType::MessageType_NEMDecryptedMessage => 124,
MessageType::MessageType_TezosSignTx => 125, MessageType::MessageType_TezosGetAddress => 125,
MessageType::MessageType_TezosSignedTx => 126, MessageType::MessageType_TezosAddress => 126,
MessageType::MessageType_TezosGetPublicKey => 127, MessageType::MessageType_TezosSignTx => 127,
MessageType::MessageType_TezosPublicKey => 128, MessageType::MessageType_TezosSignedTx => 128,
MessageType::MessageType_StellarSignTx => 129, MessageType::MessageType_TezosGetPublicKey => 129,
MessageType::MessageType_StellarTxOpRequest => 130, MessageType::MessageType_TezosPublicKey => 130,
MessageType::MessageType_StellarGetAddress => 131, MessageType::MessageType_StellarSignTx => 131,
MessageType::MessageType_StellarAddress => 132, MessageType::MessageType_StellarTxOpRequest => 132,
MessageType::MessageType_StellarCreateAccountOp => 133, MessageType::MessageType_StellarGetAddress => 133,
MessageType::MessageType_StellarPaymentOp => 134, MessageType::MessageType_StellarAddress => 134,
MessageType::MessageType_StellarPathPaymentStrictReceiveOp => 135, MessageType::MessageType_StellarCreateAccountOp => 135,
MessageType::MessageType_StellarManageSellOfferOp => 136, MessageType::MessageType_StellarPaymentOp => 136,
MessageType::MessageType_StellarCreatePassiveSellOfferOp => 137, MessageType::MessageType_StellarPathPaymentStrictReceiveOp => 137,
MessageType::MessageType_StellarSetOptionsOp => 138, MessageType::MessageType_StellarManageSellOfferOp => 138,
MessageType::MessageType_StellarChangeTrustOp => 139, MessageType::MessageType_StellarCreatePassiveSellOfferOp => 139,
MessageType::MessageType_StellarAllowTrustOp => 140, MessageType::MessageType_StellarSetOptionsOp => 140,
MessageType::MessageType_StellarAccountMergeOp => 141, MessageType::MessageType_StellarChangeTrustOp => 141,
MessageType::MessageType_StellarManageDataOp => 142, MessageType::MessageType_StellarAllowTrustOp => 142,
MessageType::MessageType_StellarBumpSequenceOp => 143, MessageType::MessageType_StellarAccountMergeOp => 143,
MessageType::MessageType_StellarManageBuyOfferOp => 144, MessageType::MessageType_StellarManageDataOp => 144,
MessageType::MessageType_StellarPathPaymentStrictSendOp => 145, MessageType::MessageType_StellarBumpSequenceOp => 145,
MessageType::MessageType_StellarClaimClaimableBalanceOp => 146, MessageType::MessageType_StellarManageBuyOfferOp => 146,
MessageType::MessageType_StellarSignedTx => 147, MessageType::MessageType_StellarPathPaymentStrictSendOp => 147,
MessageType::MessageType_CardanoGetPublicKey => 148, MessageType::MessageType_StellarClaimClaimableBalanceOp => 148,
MessageType::MessageType_CardanoPublicKey => 149, MessageType::MessageType_StellarSignedTx => 149,
MessageType::MessageType_CardanoGetAddress => 150, MessageType::MessageType_CardanoGetPublicKey => 150,
MessageType::MessageType_CardanoAddress => 151, MessageType::MessageType_CardanoPublicKey => 151,
MessageType::MessageType_CardanoTxItemAck => 152, MessageType::MessageType_CardanoGetAddress => 152,
MessageType::MessageType_CardanoTxAuxiliaryDataSupplement => 153, MessageType::MessageType_CardanoAddress => 153,
MessageType::MessageType_CardanoTxWitnessRequest => 154, MessageType::MessageType_CardanoTxItemAck => 154,
MessageType::MessageType_CardanoTxWitnessResponse => 155, MessageType::MessageType_CardanoTxAuxiliaryDataSupplement => 155,
MessageType::MessageType_CardanoTxHostAck => 156, MessageType::MessageType_CardanoTxWitnessRequest => 156,
MessageType::MessageType_CardanoTxBodyHash => 157, MessageType::MessageType_CardanoTxWitnessResponse => 157,
MessageType::MessageType_CardanoSignTxFinished => 158, MessageType::MessageType_CardanoTxHostAck => 158,
MessageType::MessageType_CardanoSignTxInit => 159, MessageType::MessageType_CardanoTxBodyHash => 159,
MessageType::MessageType_CardanoTxInput => 160, MessageType::MessageType_CardanoSignTxFinished => 160,
MessageType::MessageType_CardanoTxOutput => 161, MessageType::MessageType_CardanoSignTxInit => 161,
MessageType::MessageType_CardanoAssetGroup => 162, MessageType::MessageType_CardanoTxInput => 162,
MessageType::MessageType_CardanoToken => 163, MessageType::MessageType_CardanoTxOutput => 163,
MessageType::MessageType_CardanoTxCertificate => 164, MessageType::MessageType_CardanoAssetGroup => 164,
MessageType::MessageType_CardanoTxWithdrawal => 165, MessageType::MessageType_CardanoToken => 165,
MessageType::MessageType_CardanoTxAuxiliaryData => 166, MessageType::MessageType_CardanoTxCertificate => 166,
MessageType::MessageType_CardanoPoolOwner => 167, MessageType::MessageType_CardanoTxWithdrawal => 167,
MessageType::MessageType_CardanoPoolRelayParameters => 168, MessageType::MessageType_CardanoTxAuxiliaryData => 168,
MessageType::MessageType_CardanoGetNativeScriptHash => 169, MessageType::MessageType_CardanoPoolOwner => 169,
MessageType::MessageType_CardanoNativeScriptHash => 170, MessageType::MessageType_CardanoPoolRelayParameters => 170,
MessageType::MessageType_CardanoTxMint => 171, MessageType::MessageType_CardanoGetNativeScriptHash => 171,
MessageType::MessageType_CardanoTxCollateralInput => 172, MessageType::MessageType_CardanoNativeScriptHash => 172,
MessageType::MessageType_CardanoTxRequiredSigner => 173, MessageType::MessageType_CardanoTxMint => 173,
MessageType::MessageType_CardanoTxInlineDatumChunk => 174, MessageType::MessageType_CardanoTxCollateralInput => 174,
MessageType::MessageType_CardanoTxReferenceScriptChunk => 175, MessageType::MessageType_CardanoTxRequiredSigner => 175,
MessageType::MessageType_CardanoTxReferenceInput => 176, MessageType::MessageType_CardanoTxInlineDatumChunk => 176,
MessageType::MessageType_RippleGetAddress => 177, MessageType::MessageType_CardanoTxReferenceScriptChunk => 177,
MessageType::MessageType_RippleAddress => 178, MessageType::MessageType_CardanoTxReferenceInput => 178,
MessageType::MessageType_RippleSignTx => 179, MessageType::MessageType_RippleGetAddress => 179,
MessageType::MessageType_RippleSignedTx => 180, MessageType::MessageType_RippleAddress => 180,
MessageType::MessageType_MoneroTransactionInitRequest => 181, MessageType::MessageType_RippleSignTx => 181,
MessageType::MessageType_MoneroTransactionInitAck => 182, MessageType::MessageType_RippleSignedTx => 182,
MessageType::MessageType_MoneroTransactionSetInputRequest => 183, MessageType::MessageType_MoneroTransactionInitRequest => 183,
MessageType::MessageType_MoneroTransactionSetInputAck => 184, MessageType::MessageType_MoneroTransactionInitAck => 184,
MessageType::MessageType_MoneroTransactionInputViniRequest => 185, MessageType::MessageType_MoneroTransactionSetInputRequest => 185,
MessageType::MessageType_MoneroTransactionInputViniAck => 186, MessageType::MessageType_MoneroTransactionSetInputAck => 186,
MessageType::MessageType_MoneroTransactionAllInputsSetRequest => 187, MessageType::MessageType_MoneroTransactionInputViniRequest => 187,
MessageType::MessageType_MoneroTransactionAllInputsSetAck => 188, MessageType::MessageType_MoneroTransactionInputViniAck => 188,
MessageType::MessageType_MoneroTransactionSetOutputRequest => 189, MessageType::MessageType_MoneroTransactionAllInputsSetRequest => 189,
MessageType::MessageType_MoneroTransactionSetOutputAck => 190, MessageType::MessageType_MoneroTransactionAllInputsSetAck => 190,
MessageType::MessageType_MoneroTransactionAllOutSetRequest => 191, MessageType::MessageType_MoneroTransactionSetOutputRequest => 191,
MessageType::MessageType_MoneroTransactionAllOutSetAck => 192, MessageType::MessageType_MoneroTransactionSetOutputAck => 192,
MessageType::MessageType_MoneroTransactionSignInputRequest => 193, MessageType::MessageType_MoneroTransactionAllOutSetRequest => 193,
MessageType::MessageType_MoneroTransactionSignInputAck => 194, MessageType::MessageType_MoneroTransactionAllOutSetAck => 194,
MessageType::MessageType_MoneroTransactionFinalRequest => 195, MessageType::MessageType_MoneroTransactionSignInputRequest => 195,
MessageType::MessageType_MoneroTransactionFinalAck => 196, MessageType::MessageType_MoneroTransactionSignInputAck => 196,
MessageType::MessageType_MoneroKeyImageExportInitRequest => 197, MessageType::MessageType_MoneroTransactionFinalRequest => 197,
MessageType::MessageType_MoneroKeyImageExportInitAck => 198, MessageType::MessageType_MoneroTransactionFinalAck => 198,
MessageType::MessageType_MoneroKeyImageSyncStepRequest => 199, MessageType::MessageType_MoneroKeyImageExportInitRequest => 199,
MessageType::MessageType_MoneroKeyImageSyncStepAck => 200, MessageType::MessageType_MoneroKeyImageExportInitAck => 200,
MessageType::MessageType_MoneroKeyImageSyncFinalRequest => 201, MessageType::MessageType_MoneroKeyImageSyncStepRequest => 201,
MessageType::MessageType_MoneroKeyImageSyncFinalAck => 202, MessageType::MessageType_MoneroKeyImageSyncStepAck => 202,
MessageType::MessageType_MoneroGetAddress => 203, MessageType::MessageType_MoneroKeyImageSyncFinalRequest => 203,
MessageType::MessageType_MoneroAddress => 204, MessageType::MessageType_MoneroKeyImageSyncFinalAck => 204,
MessageType::MessageType_MoneroGetWatchKey => 205, MessageType::MessageType_MoneroGetAddress => 205,
MessageType::MessageType_MoneroWatchKey => 206, MessageType::MessageType_MoneroAddress => 206,
MessageType::MessageType_DebugMoneroDiagRequest => 207, MessageType::MessageType_MoneroGetWatchKey => 207,
MessageType::MessageType_DebugMoneroDiagAck => 208, MessageType::MessageType_MoneroWatchKey => 208,
MessageType::MessageType_MoneroGetTxKeyRequest => 209, MessageType::MessageType_DebugMoneroDiagRequest => 209,
MessageType::MessageType_MoneroGetTxKeyAck => 210, MessageType::MessageType_DebugMoneroDiagAck => 210,
MessageType::MessageType_MoneroLiveRefreshStartRequest => 211, MessageType::MessageType_MoneroGetTxKeyRequest => 211,
MessageType::MessageType_MoneroLiveRefreshStartAck => 212, MessageType::MessageType_MoneroGetTxKeyAck => 212,
MessageType::MessageType_MoneroLiveRefreshStepRequest => 213, MessageType::MessageType_MoneroLiveRefreshStartRequest => 213,
MessageType::MessageType_MoneroLiveRefreshStepAck => 214, MessageType::MessageType_MoneroLiveRefreshStartAck => 214,
MessageType::MessageType_MoneroLiveRefreshFinalRequest => 215, MessageType::MessageType_MoneroLiveRefreshStepRequest => 215,
MessageType::MessageType_MoneroLiveRefreshFinalAck => 216, MessageType::MessageType_MoneroLiveRefreshStepAck => 216,
MessageType::MessageType_EosGetPublicKey => 217, MessageType::MessageType_MoneroLiveRefreshFinalRequest => 217,
MessageType::MessageType_EosPublicKey => 218, MessageType::MessageType_MoneroLiveRefreshFinalAck => 218,
MessageType::MessageType_EosSignTx => 219, MessageType::MessageType_EosGetPublicKey => 219,
MessageType::MessageType_EosTxActionRequest => 220, MessageType::MessageType_EosPublicKey => 220,
MessageType::MessageType_EosTxActionAck => 221, MessageType::MessageType_EosSignTx => 221,
MessageType::MessageType_EosSignedTx => 222, MessageType::MessageType_EosTxActionRequest => 222,
MessageType::MessageType_BinanceGetAddress => 223, MessageType::MessageType_EosTxActionAck => 223,
MessageType::MessageType_BinanceAddress => 224, MessageType::MessageType_EosSignedTx => 224,
MessageType::MessageType_BinanceGetPublicKey => 225, MessageType::MessageType_BinanceGetAddress => 225,
MessageType::MessageType_BinancePublicKey => 226, MessageType::MessageType_BinanceAddress => 226,
MessageType::MessageType_BinanceSignTx => 227, MessageType::MessageType_BinanceGetPublicKey => 227,
MessageType::MessageType_BinanceTxRequest => 228, MessageType::MessageType_BinancePublicKey => 228,
MessageType::MessageType_BinanceTransferMsg => 229, MessageType::MessageType_BinanceSignTx => 229,
MessageType::MessageType_BinanceOrderMsg => 230, MessageType::MessageType_BinanceTxRequest => 230,
MessageType::MessageType_BinanceCancelMsg => 231, MessageType::MessageType_BinanceTransferMsg => 231,
MessageType::MessageType_BinanceSignedTx => 232, MessageType::MessageType_BinanceOrderMsg => 232,
MessageType::MessageType_WebAuthnListResidentCredentials => 233, MessageType::MessageType_BinanceCancelMsg => 233,
MessageType::MessageType_WebAuthnCredentials => 234, MessageType::MessageType_BinanceSignedTx => 234,
MessageType::MessageType_WebAuthnAddResidentCredential => 235, MessageType::MessageType_WebAuthnListResidentCredentials => 235,
MessageType::MessageType_WebAuthnRemoveResidentCredential => 236, MessageType::MessageType_WebAuthnCredentials => 236,
MessageType::MessageType_SolanaGetPublicKey => 237, MessageType::MessageType_WebAuthnAddResidentCredential => 237,
MessageType::MessageType_SolanaPublicKey => 238, MessageType::MessageType_WebAuthnRemoveResidentCredential => 238,
MessageType::MessageType_SolanaGetAddress => 239, MessageType::MessageType_SolanaGetPublicKey => 239,
MessageType::MessageType_SolanaAddress => 240, MessageType::MessageType_SolanaPublicKey => 240,
MessageType::MessageType_SolanaSignTx => 241, MessageType::MessageType_SolanaGetAddress => 241,
MessageType::MessageType_SolanaTxSignature => 242, MessageType::MessageType_SolanaAddress => 242,
MessageType::MessageType_BenchmarkListNames => 243, MessageType::MessageType_SolanaSignTx => 243,
MessageType::MessageType_BenchmarkNames => 244, MessageType::MessageType_SolanaTxSignature => 244,
MessageType::MessageType_BenchmarkRun => 245, MessageType::MessageType_BenchmarkListNames => 245,
MessageType::MessageType_BenchmarkResult => 246, MessageType::MessageType_BenchmarkNames => 246,
MessageType::MessageType_BenchmarkRun => 247,
MessageType::MessageType_BenchmarkResult => 248,
}; };
Self::enum_descriptor().value_by_index(index) Self::enum_descriptor().value_by_index(index)
} }
@ -1561,7 +1573,7 @@ impl MessageType {
} }
static file_descriptor_proto_data: &'static [u8] = b"\ static file_descriptor_proto_data: &'static [u8] = b"\
\n\x0emessages.proto\x12\x12hw.trezor.messages\x1a\roptions.proto*\xe8U\ \n\x0emessages.proto\x12\x12hw.trezor.messages\x1a\roptions.proto*\xcdV\
\n\x0bMessageType\x12(\n\x16MessageType_Initialize\x10\0\x1a\x0c\x80\xa6\ \n\x0bMessageType\x12(\n\x16MessageType_Initialize\x10\0\x1a\x0c\x80\xa6\
\x1d\x01\xb0\xb5\x18\x01\x90\xb5\x18\x01\x12\x1e\n\x10MessageType_Ping\ \x1d\x01\xb0\xb5\x18\x01\x90\xb5\x18\x01\x12\x1e\n\x10MessageType_Ping\
\x10\x01\x1a\x08\x80\xa6\x1d\x01\x90\xb5\x18\x01\x12%\n\x13MessageType_S\ \x10\x01\x1a\x08\x80\xa6\x1d\x01\x90\xb5\x18\x01\x12%\n\x13MessageType_S\
@ -1681,172 +1693,174 @@ static file_descriptor_proto_data: &'static [u8] = b"\
\x1d\x01\xa0\xb5\x18\x01\x124\n%MessageType_DebugLinkResetDebugEvents\ \x1d\x01\xa0\xb5\x18\x01\x124\n%MessageType_DebugLinkResetDebugEvents\
\x10\xafF\x1a\x08\x80\xa6\x1d\x01\xa0\xb5\x18\x01\x123\n$MessageType_Deb\ \x10\xafF\x1a\x08\x80\xa6\x1d\x01\xa0\xb5\x18\x01\x123\n$MessageType_Deb\
ugLinkOptigaSetSecMax\x10\xb0F\x1a\x08\x80\xa6\x1d\x01\xa0\xb5\x18\x01\ ugLinkOptigaSetSecMax\x10\xb0F\x1a\x08\x80\xa6\x1d\x01\xa0\xb5\x18\x01\
\x12+\n\x20MessageType_EthereumGetPublicKey\x10\xc2\x03\x1a\x04\x90\xb5\ \x122\n#MessageType_DebugLinkGetPairingInfo\x10\xb1F\x1a\x08\x80\xa6\x1d\
\x18\x01\x12(\n\x1dMessageType_EthereumPublicKey\x10\xc3\x03\x1a\x04\x98\ \x01\xa0\xb5\x18\x01\x12/\n\x20MessageType_DebugLinkPairingInfo\x10\xb2F\
\xb5\x18\x01\x12(\n\x1eMessageType_EthereumGetAddress\x108\x1a\x04\x90\ \x1a\x08\x80\xa6\x1d\x01\xa8\xb5\x18\x01\x12+\n\x20MessageType_EthereumG\
\xb5\x18\x01\x12%\n\x1bMessageType_EthereumAddress\x109\x1a\x04\x98\xb5\ etPublicKey\x10\xc2\x03\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_Et\
\x18\x01\x12$\n\x1aMessageType_EthereumSignTx\x10:\x1a\x04\x90\xb5\x18\ hereumPublicKey\x10\xc3\x03\x1a\x04\x98\xb5\x18\x01\x12(\n\x1eMessageTyp\
\x01\x12,\n!MessageType_EthereumSignTxEIP1559\x10\xc4\x03\x1a\x04\x90\ e_EthereumGetAddress\x108\x1a\x04\x90\xb5\x18\x01\x12%\n\x1bMessageType_\
\xb5\x18\x01\x12'\n\x1dMessageType_EthereumTxRequest\x10;\x1a\x04\x98\ EthereumAddress\x109\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_Ether\
\xb5\x18\x01\x12#\n\x19MessageType_EthereumTxAck\x10<\x1a\x04\x90\xb5\ eumSignTx\x10:\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_EthereumSignTx\
\x18\x01\x12)\n\x1fMessageType_EthereumSignMessage\x10@\x1a\x04\x90\xb5\ EIP1559\x10\xc4\x03\x1a\x04\x90\xb5\x18\x01\x12'\n\x1dMessageType_Ethere\
\x18\x01\x12+\n!MessageType_EthereumVerifyMessage\x10A\x1a\x04\x90\xb5\ umTxRequest\x10;\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_EthereumT\
\x18\x01\x12.\n$MessageType_EthereumMessageSignature\x10B\x1a\x04\x98\ xAck\x10<\x1a\x04\x90\xb5\x18\x01\x12)\n\x1fMessageType_EthereumSignMess\
\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\x10\xd0\x03\x1a\ age\x10@\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumVerifyMessag\
\x04\x90\xb5\x18\x01\x125\n*MessageType_EthereumTypedDataStructRequest\ e\x10A\x1a\x04\x90\xb5\x18\x01\x12.\n$MessageType_EthereumMessageSignatu\
\x10\xd1\x03\x1a\x04\x98\xb5\x18\x01\x121\n&MessageType_EthereumTypedDat\ re\x10B\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\
aStructAck\x10\xd2\x03\x1a\x04\x90\xb5\x18\x01\x124\n)MessageType_Ethere\ \x10\xd0\x03\x1a\x04\x90\xb5\x18\x01\x125\n*MessageType_EthereumTypedDat\
umTypedDataValueRequest\x10\xd3\x03\x1a\x04\x98\xb5\x18\x01\x120\n%Messa\ aStructRequest\x10\xd1\x03\x1a\x04\x98\xb5\x18\x01\x121\n&MessageType_Et\
geType_EthereumTypedDataValueAck\x10\xd4\x03\x1a\x04\x90\xb5\x18\x01\x12\ hereumTypedDataStructAck\x10\xd2\x03\x1a\x04\x90\xb5\x18\x01\x124\n)Mess\
1\n&MessageType_EthereumTypedDataSignature\x10\xd5\x03\x1a\x04\x98\xb5\ ageType_EthereumTypedDataValueRequest\x10\xd3\x03\x1a\x04\x98\xb5\x18\
\x18\x01\x12,\n!MessageType_EthereumSignTypedHash\x10\xd6\x03\x1a\x04\ \x01\x120\n%MessageType_EthereumTypedDataValueAck\x10\xd4\x03\x1a\x04\
\x90\xb5\x18\x01\x12#\n\x19MessageType_NEMGetAddress\x10C\x1a\x04\x90\ \x90\xb5\x18\x01\x121\n&MessageType_EthereumTypedDataSignature\x10\xd5\
\xb5\x18\x01\x12\x20\n\x16MessageType_NEMAddress\x10D\x1a\x04\x98\xb5\ \x03\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedHash\
\x18\x01\x12\x1f\n\x15MessageType_NEMSignTx\x10E\x1a\x04\x90\xb5\x18\x01\ \x10\xd6\x03\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_NEMGetAddress\
\x12!\n\x17MessageType_NEMSignedTx\x10F\x1a\x04\x98\xb5\x18\x01\x12'\n\ \x10C\x1a\x04\x90\xb5\x18\x01\x12\x20\n\x16MessageType_NEMAddress\x10D\
\x1dMessageType_NEMDecryptMessage\x10K\x1a\x04\x90\xb5\x18\x01\x12)\n\ \x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_NEMSignTx\x10E\x1a\x04\
\x1fMessageType_NEMDecryptedMessage\x10L\x1a\x04\x98\xb5\x18\x01\x12&\n\ \x90\xb5\x18\x01\x12!\n\x17MessageType_NEMSignedTx\x10F\x1a\x04\x98\xb5\
\x1bMessageType_TezosGetAddress\x10\x96\x01\x1a\x04\x90\xb5\x18\x01\x12#\ \x18\x01\x12'\n\x1dMessageType_NEMDecryptMessage\x10K\x1a\x04\x90\xb5\
\n\x18MessageType_TezosAddress\x10\x97\x01\x1a\x04\x98\xb5\x18\x01\x12\"\ \x18\x01\x12)\n\x1fMessageType_NEMDecryptedMessage\x10L\x1a\x04\x98\xb5\
\n\x17MessageType_TezosSignTx\x10\x98\x01\x1a\x04\x90\xb5\x18\x01\x12$\n\ \x18\x01\x12&\n\x1bMessageType_TezosGetAddress\x10\x96\x01\x1a\x04\x90\
\x19MessageType_TezosSignedTx\x10\x99\x01\x1a\x04\x98\xb5\x18\x01\x12(\n\ \xb5\x18\x01\x12#\n\x18MessageType_TezosAddress\x10\x97\x01\x1a\x04\x98\
\x1dMessageType_TezosGetPublicKey\x10\x9a\x01\x1a\x04\x90\xb5\x18\x01\ \xb5\x18\x01\x12\"\n\x17MessageType_TezosSignTx\x10\x98\x01\x1a\x04\x90\
\x12%\n\x1aMessageType_TezosPublicKey\x10\x9b\x01\x1a\x04\x98\xb5\x18\ \xb5\x18\x01\x12$\n\x19MessageType_TezosSignedTx\x10\x99\x01\x1a\x04\x98\
\x01\x12$\n\x19MessageType_StellarSignTx\x10\xca\x01\x1a\x04\x90\xb5\x18\ \xb5\x18\x01\x12(\n\x1dMessageType_TezosGetPublicKey\x10\x9a\x01\x1a\x04\
\x01\x12)\n\x1eMessageType_StellarTxOpRequest\x10\xcb\x01\x1a\x04\x98\ \x90\xb5\x18\x01\x12%\n\x1aMessageType_TezosPublicKey\x10\x9b\x01\x1a\
\xb5\x18\x01\x12(\n\x1dMessageType_StellarGetAddress\x10\xcf\x01\x1a\x04\ \x04\x98\xb5\x18\x01\x12$\n\x19MessageType_StellarSignTx\x10\xca\x01\x1a\
\x90\xb5\x18\x01\x12%\n\x1aMessageType_StellarAddress\x10\xd0\x01\x1a\ \x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_StellarTxOpRequest\x10\xcb\
\x04\x98\xb5\x18\x01\x12-\n\"MessageType_StellarCreateAccountOp\x10\xd2\ \x01\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_StellarGetAddress\x10\
\x01\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_StellarPaymentOp\x10\ \xcf\x01\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_StellarAddress\
\xd3\x01\x1a\x04\x90\xb5\x18\x01\x128\n-MessageType_StellarPathPaymentSt\ \x10\xd0\x01\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_StellarCreateAc\
rictReceiveOp\x10\xd4\x01\x1a\x04\x90\xb5\x18\x01\x12/\n$MessageType_Ste\ countOp\x10\xd2\x01\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_Stella\
llarManageSellOfferOp\x10\xd5\x01\x1a\x04\x90\xb5\x18\x01\x126\n+Message\ rPaymentOp\x10\xd3\x01\x1a\x04\x90\xb5\x18\x01\x128\n-MessageType_Stella\
Type_StellarCreatePassiveSellOfferOp\x10\xd6\x01\x1a\x04\x90\xb5\x18\x01\ rPathPaymentStrictReceiveOp\x10\xd4\x01\x1a\x04\x90\xb5\x18\x01\x12/\n$M\
\x12*\n\x1fMessageType_StellarSetOptionsOp\x10\xd7\x01\x1a\x04\x90\xb5\ essageType_StellarManageSellOfferOp\x10\xd5\x01\x1a\x04\x90\xb5\x18\x01\
\x18\x01\x12+\n\x20MessageType_StellarChangeTrustOp\x10\xd8\x01\x1a\x04\ \x126\n+MessageType_StellarCreatePassiveSellOfferOp\x10\xd6\x01\x1a\x04\
\x90\xb5\x18\x01\x12*\n\x1fMessageType_StellarAllowTrustOp\x10\xd9\x01\ \x90\xb5\x18\x01\x12*\n\x1fMessageType_StellarSetOptionsOp\x10\xd7\x01\
\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_StellarAccountMergeOp\x10\ \x1a\x04\x90\xb5\x18\x01\x12+\n\x20MessageType_StellarChangeTrustOp\x10\
\xda\x01\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_StellarManageData\ \xd8\x01\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_StellarAllowTrust\
Op\x10\xdc\x01\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_StellarBumpSeq\ Op\x10\xd9\x01\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_StellarAccount\
uenceOp\x10\xdd\x01\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_StellarMa\ MergeOp\x10\xda\x01\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_Stella\
nageBuyOfferOp\x10\xde\x01\x1a\x04\x90\xb5\x18\x01\x125\n*MessageType_St\ rManageDataOp\x10\xdc\x01\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_Ste\
ellarPathPaymentStrictSendOp\x10\xdf\x01\x1a\x04\x90\xb5\x18\x01\x125\n*\ llarBumpSequenceOp\x10\xdd\x01\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageTyp\
MessageType_StellarClaimClaimableBalanceOp\x10\xe1\x01\x1a\x04\x90\xb5\ e_StellarManageBuyOfferOp\x10\xde\x01\x1a\x04\x90\xb5\x18\x01\x125\n*Mes\
\x18\x01\x12&\n\x1bMessageType_StellarSignedTx\x10\xe6\x01\x1a\x04\x98\ sageType_StellarPathPaymentStrictSendOp\x10\xdf\x01\x1a\x04\x90\xb5\x18\
\xb5\x18\x01\x12*\n\x1fMessageType_CardanoGetPublicKey\x10\xb1\x02\x1a\ \x01\x125\n*MessageType_StellarClaimClaimableBalanceOp\x10\xe1\x01\x1a\
\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_CardanoPublicKey\x10\xb2\x02\ \x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_StellarSignedTx\x10\xe6\x01\
\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CardanoGetAddress\x10\xb3\ \x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CardanoGetPublicKey\x10\
\x02\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CardanoAddress\x10\ \xb1\x02\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_CardanoPublicKey\
\xb4\x02\x1a\x04\x98\xb5\x18\x01\x12'\n\x1cMessageType_CardanoTxItemAck\ \x10\xb2\x02\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CardanoGetAdd\
\x10\xb9\x02\x1a\x04\x98\xb5\x18\x01\x127\n,MessageType_CardanoTxAuxilia\ ress\x10\xb3\x02\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CardanoAd\
ryDataSupplement\x10\xba\x02\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_\ dress\x10\xb4\x02\x1a\x04\x98\xb5\x18\x01\x12'\n\x1cMessageType_CardanoT\
CardanoTxWitnessRequest\x10\xbb\x02\x1a\x04\x90\xb5\x18\x01\x12/\n$Messa\ xItemAck\x10\xb9\x02\x1a\x04\x98\xb5\x18\x01\x127\n,MessageType_CardanoT\
geType_CardanoTxWitnessResponse\x10\xbc\x02\x1a\x04\x98\xb5\x18\x01\x12'\ xAuxiliaryDataSupplement\x10\xba\x02\x1a\x04\x98\xb5\x18\x01\x12.\n#Mess\
\n\x1cMessageType_CardanoTxHostAck\x10\xbd\x02\x1a\x04\x90\xb5\x18\x01\ ageType_CardanoTxWitnessRequest\x10\xbb\x02\x1a\x04\x90\xb5\x18\x01\x12/\
\x12(\n\x1dMessageType_CardanoTxBodyHash\x10\xbe\x02\x1a\x04\x98\xb5\x18\ \n$MessageType_CardanoTxWitnessResponse\x10\xbc\x02\x1a\x04\x98\xb5\x18\
\x01\x12,\n!MessageType_CardanoSignTxFinished\x10\xbf\x02\x1a\x04\x98\ \x01\x12'\n\x1cMessageType_CardanoTxHostAck\x10\xbd\x02\x1a\x04\x90\xb5\
\xb5\x18\x01\x12(\n\x1dMessageType_CardanoSignTxInit\x10\xc0\x02\x1a\x04\ \x18\x01\x12(\n\x1dMessageType_CardanoTxBodyHash\x10\xbe\x02\x1a\x04\x98\
\x90\xb5\x18\x01\x12%\n\x1aMessageType_CardanoTxInput\x10\xc1\x02\x1a\ \xb5\x18\x01\x12,\n!MessageType_CardanoSignTxFinished\x10\xbf\x02\x1a\
\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_CardanoTxOutput\x10\xc2\x02\ \x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CardanoSignTxInit\x10\xc0\x02\
\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_CardanoAssetGroup\x10\xc3\ \x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CardanoTxInput\x10\xc1\
\x02\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_CardanoToken\x10\xc4\ \x02\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_CardanoTxOutput\x10\
\x02\x1a\x04\x90\xb5\x18\x01\x12+\n\x20MessageType_CardanoTxCertificate\ \xc2\x02\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_CardanoAssetGroup\
\x10\xc5\x02\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_CardanoTxWith\ \x10\xc3\x02\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_CardanoToken\
drawal\x10\xc6\x02\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_CardanoTx\ \x10\xc4\x02\x1a\x04\x90\xb5\x18\x01\x12+\n\x20MessageType_CardanoTxCert\
AuxiliaryData\x10\xc7\x02\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_\ ificate\x10\xc5\x02\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_Cardan\
CardanoPoolOwner\x10\xc8\x02\x1a\x04\x90\xb5\x18\x01\x121\n&MessageType_\ oTxWithdrawal\x10\xc6\x02\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_Ca\
CardanoPoolRelayParameters\x10\xc9\x02\x1a\x04\x90\xb5\x18\x01\x121\n&Me\ rdanoTxAuxiliaryData\x10\xc7\x02\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessa\
ssageType_CardanoGetNativeScriptHash\x10\xca\x02\x1a\x04\x90\xb5\x18\x01\ geType_CardanoPoolOwner\x10\xc8\x02\x1a\x04\x90\xb5\x18\x01\x121\n&Messa\
\x12.\n#MessageType_CardanoNativeScriptHash\x10\xcb\x02\x1a\x04\x98\xb5\ geType_CardanoPoolRelayParameters\x10\xc9\x02\x1a\x04\x90\xb5\x18\x01\
\x18\x01\x12$\n\x19MessageType_CardanoTxMint\x10\xcc\x02\x1a\x04\x90\xb5\ \x121\n&MessageType_CardanoGetNativeScriptHash\x10\xca\x02\x1a\x04\x90\
\x18\x01\x12/\n$MessageType_CardanoTxCollateralInput\x10\xcd\x02\x1a\x04\ \xb5\x18\x01\x12.\n#MessageType_CardanoNativeScriptHash\x10\xcb\x02\x1a\
\x90\xb5\x18\x01\x12.\n#MessageType_CardanoTxRequiredSigner\x10\xce\x02\ \x04\x98\xb5\x18\x01\x12$\n\x19MessageType_CardanoTxMint\x10\xcc\x02\x1a\
\x1a\x04\x90\xb5\x18\x01\x120\n%MessageType_CardanoTxInlineDatumChunk\ \x04\x90\xb5\x18\x01\x12/\n$MessageType_CardanoTxCollateralInput\x10\xcd\
\x10\xcf\x02\x1a\x04\x90\xb5\x18\x01\x124\n)MessageType_CardanoTxReferen\ \x02\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_CardanoTxRequiredSigner\
ceScriptChunk\x10\xd0\x02\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_Car\ \x10\xce\x02\x1a\x04\x90\xb5\x18\x01\x120\n%MessageType_CardanoTxInlineD\
danoTxReferenceInput\x10\xd1\x02\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessa\ atumChunk\x10\xcf\x02\x1a\x04\x90\xb5\x18\x01\x124\n)MessageType_Cardano\
geType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12$\n\x19Me\ TxReferenceScriptChunk\x10\xd0\x02\x1a\x04\x90\xb5\x18\x01\x12.\n#Messag\
ssageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12#\n\x18Me\ eType_CardanoTxReferenceInput\x10\xd1\x02\x1a\x04\x90\xb5\x18\x01\x12'\n\
ssageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMes\ \x1cMessageType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12\
sageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x123\n(Messa\ $\n\x19MessageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12\
geType_MoneroTransactionInitRequest\x10\xf5\x03\x1a\x04\x98\xb5\x18\x01\ #\n\x18MessageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\
\x12/\n$MessageType_MoneroTransactionInitAck\x10\xf6\x03\x1a\x04\x98\xb5\ \n\x1aMessageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x12\
\x18\x01\x127\n,MessageType_MoneroTransactionSetInputRequest\x10\xf7\x03\ 3\n(MessageType_MoneroTransactionInitRequest\x10\xf5\x03\x1a\x04\x98\xb5\
\x1a\x04\x98\xb5\x18\x01\x123\n(MessageType_MoneroTransactionSetInputAck\ \x18\x01\x12/\n$MessageType_MoneroTransactionInitAck\x10\xf6\x03\x1a\x04\
\x10\xf8\x03\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTransactio\ \x98\xb5\x18\x01\x127\n,MessageType_MoneroTransactionSetInputRequest\x10\
nInputViniRequest\x10\xfb\x03\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType\ \xf7\x03\x1a\x04\x98\xb5\x18\x01\x123\n(MessageType_MoneroTransactionSet\
_MoneroTransactionInputViniAck\x10\xfc\x03\x1a\x04\x98\xb5\x18\x01\x12;\ InputAck\x10\xf8\x03\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTr\
\n0MessageType_MoneroTransactionAllInputsSetRequest\x10\xfd\x03\x1a\x04\ ansactionInputViniRequest\x10\xfb\x03\x1a\x04\x98\xb5\x18\x01\x124\n)Mes\
\x98\xb5\x18\x01\x127\n,MessageType_MoneroTransactionAllInputsSetAck\x10\ sageType_MoneroTransactionInputViniAck\x10\xfc\x03\x1a\x04\x98\xb5\x18\
\xfe\x03\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTransactionSet\ \x01\x12;\n0MessageType_MoneroTransactionAllInputsSetRequest\x10\xfd\x03\
OutputRequest\x10\xff\x03\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_Mon\ \x1a\x04\x98\xb5\x18\x01\x127\n,MessageType_MoneroTransactionAllInputsSe\
eroTransactionSetOutputAck\x10\x80\x04\x1a\x04\x98\xb5\x18\x01\x128\n-Me\ tAck\x10\xfe\x03\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTransa\
ssageType_MoneroTransactionAllOutSetRequest\x10\x81\x04\x1a\x04\x98\xb5\ ctionSetOutputRequest\x10\xff\x03\x1a\x04\x98\xb5\x18\x01\x124\n)Message\
\x18\x01\x124\n)MessageType_MoneroTransactionAllOutSetAck\x10\x82\x04\ Type_MoneroTransactionSetOutputAck\x10\x80\x04\x1a\x04\x98\xb5\x18\x01\
\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTransactionSignInputRe\ \x128\n-MessageType_MoneroTransactionAllOutSetRequest\x10\x81\x04\x1a\
quest\x10\x83\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_MoneroTrans\ \x04\x98\xb5\x18\x01\x124\n)MessageType_MoneroTransactionAllOutSetAck\
actionSignInputAck\x10\x84\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageTyp\ \x10\x82\x04\x1a\x04\x98\xb5\x18\x01\x128\n-MessageType_MoneroTransactio\
e_MoneroTransactionFinalRequest\x10\x85\x04\x1a\x04\x98\xb5\x18\x01\x120\ nSignInputRequest\x10\x83\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType\
\n%MessageType_MoneroTransactionFinalAck\x10\x86\x04\x1a\x04\x98\xb5\x18\ _MoneroTransactionSignInputAck\x10\x84\x04\x1a\x04\x98\xb5\x18\x01\x124\
\x01\x126\n+MessageType_MoneroKeyImageExportInitRequest\x10\x92\x04\x1a\ \n)MessageType_MoneroTransactionFinalRequest\x10\x85\x04\x1a\x04\x98\xb5\
\x04\x98\xb5\x18\x01\x122\n'MessageType_MoneroKeyImageExportInitAck\x10\ \x18\x01\x120\n%MessageType_MoneroTransactionFinalAck\x10\x86\x04\x1a\
\x93\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_MoneroKeyImageSyncSt\ \x04\x98\xb5\x18\x01\x126\n+MessageType_MoneroKeyImageExportInitRequest\
epRequest\x10\x94\x04\x1a\x04\x98\xb5\x18\x01\x120\n%MessageType_MoneroK\ \x10\x92\x04\x1a\x04\x98\xb5\x18\x01\x122\n'MessageType_MoneroKeyImageEx\
eyImageSyncStepAck\x10\x95\x04\x1a\x04\x98\xb5\x18\x01\x125\n*MessageTyp\ portInitAck\x10\x93\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_Moner\
e_MoneroKeyImageSyncFinalRequest\x10\x96\x04\x1a\x04\x98\xb5\x18\x01\x12\ oKeyImageSyncStepRequest\x10\x94\x04\x1a\x04\x98\xb5\x18\x01\x120\n%Mess\
1\n&MessageType_MoneroKeyImageSyncFinalAck\x10\x97\x04\x1a\x04\x98\xb5\ ageType_MoneroKeyImageSyncStepAck\x10\x95\x04\x1a\x04\x98\xb5\x18\x01\
\x18\x01\x12'\n\x1cMessageType_MoneroGetAddress\x10\x9c\x04\x1a\x04\x90\ \x125\n*MessageType_MoneroKeyImageSyncFinalRequest\x10\x96\x04\x1a\x04\
\xb5\x18\x01\x12$\n\x19MessageType_MoneroAddress\x10\x9d\x04\x1a\x04\x98\ \x98\xb5\x18\x01\x121\n&MessageType_MoneroKeyImageSyncFinalAck\x10\x97\
\xb5\x18\x01\x12(\n\x1dMessageType_MoneroGetWatchKey\x10\x9e\x04\x1a\x04\ \x04\x1a\x04\x98\xb5\x18\x01\x12'\n\x1cMessageType_MoneroGetAddress\x10\
\x90\xb5\x18\x01\x12%\n\x1aMessageType_MoneroWatchKey\x10\x9f\x04\x1a\ \x9c\x04\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_MoneroAddress\x10\
\x04\x98\xb5\x18\x01\x12-\n\"MessageType_DebugMoneroDiagRequest\x10\xa2\ \x9d\x04\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_MoneroGetWatchKey\
\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_DebugMoneroDiagAck\ \x10\x9e\x04\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_MoneroWatchKe\
\x10\xa3\x04\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_MoneroGetTxKeyRe\ y\x10\x9f\x04\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_DebugMoneroDia\
quest\x10\xa6\x04\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_MoneroGe\ gRequest\x10\xa2\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_Debug\
tTxKeyAck\x10\xa7\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_MoneroL\ MoneroDiagAck\x10\xa3\x04\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_Mon\
iveRefreshStartRequest\x10\xa8\x04\x1a\x04\x90\xb5\x18\x01\x120\n%Messag\ eroGetTxKeyRequest\x10\xa6\x04\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessage\
eType_MoneroLiveRefreshStartAck\x10\xa9\x04\x1a\x04\x98\xb5\x18\x01\x123\ Type_MoneroGetTxKeyAck\x10\xa7\x04\x1a\x04\x98\xb5\x18\x01\x124\n)Messag\
\n(MessageType_MoneroLiveRefreshStepRequest\x10\xaa\x04\x1a\x04\x90\xb5\ eType_MoneroLiveRefreshStartRequest\x10\xa8\x04\x1a\x04\x90\xb5\x18\x01\
\x18\x01\x12/\n$MessageType_MoneroLiveRefreshStepAck\x10\xab\x04\x1a\x04\ \x120\n%MessageType_MoneroLiveRefreshStartAck\x10\xa9\x04\x1a\x04\x98\
\x98\xb5\x18\x01\x124\n)MessageType_MoneroLiveRefreshFinalRequest\x10\ \xb5\x18\x01\x123\n(MessageType_MoneroLiveRefreshStepRequest\x10\xaa\x04\
\xac\x04\x1a\x04\x90\xb5\x18\x01\x120\n%MessageType_MoneroLiveRefreshFin\ \x1a\x04\x90\xb5\x18\x01\x12/\n$MessageType_MoneroLiveRefreshStepAck\x10\
alAck\x10\xad\x04\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_EosGetPu\ \xab\x04\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_MoneroLiveRefreshFin\
blicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_EosPub\ alRequest\x10\xac\x04\x1a\x04\x90\xb5\x18\x01\x120\n%MessageType_MoneroL\
licKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12\x20\n\x15MessageType_EosS\ iveRefreshFinalAck\x10\xad\x04\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessage\
ignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_EosTxAct\ Type_EosGetPublicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18Messa\
ionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_Eos\ geType_EosPublicKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12\x20\n\x15Mes\
TxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_E\ sageType_EosSignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessage\
osSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_Bin\ Type_EosTxActionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMe\
anceGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType\ ssageType_EosTxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17\
_BinanceAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageTyp\ MessageType_EosSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMe\
e_BinanceGetPublicKey\x10\xbe\x05\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMess\ ssageType_BinanceGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\
ageType_BinancePublicKey\x10\xbf\x05\x1a\x04\x98\xb5\x18\x01\x12$\n\x19M\ \x1aMessageType_BinanceAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12*\
essageType_BinanceSignTx\x10\xc0\x05\x1a\x04\x90\xb5\x18\x01\x12'\n\x1cM\ \n\x1fMessageType_BinanceGetPublicKey\x10\xbe\x05\x1a\x04\x90\xb5\x18\
essageType_BinanceTxRequest\x10\xc1\x05\x1a\x04\x98\xb5\x18\x01\x12)\n\ \x01\x12'\n\x1cMessageType_BinancePublicKey\x10\xbf\x05\x1a\x04\x98\xb5\
\x1eMessageType_BinanceTransferMsg\x10\xc2\x05\x1a\x04\x90\xb5\x18\x01\ \x18\x01\x12$\n\x19MessageType_BinanceSignTx\x10\xc0\x05\x1a\x04\x90\xb5\
\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xc3\x05\x1a\x04\x90\xb5\x18\ \x18\x01\x12'\n\x1cMessageType_BinanceTxRequest\x10\xc1\x05\x1a\x04\x98\
\x01\x12'\n\x1cMessageType_BinanceCancelMsg\x10\xc4\x05\x1a\x04\x90\xb5\ \xb5\x18\x01\x12)\n\x1eMessageType_BinanceTransferMsg\x10\xc2\x05\x1a\
\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\xc5\x05\x1a\x04\x98\ \x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xc3\x05\
\xb5\x18\x01\x126\n+MessageType_WebAuthnListResidentCredentials\x10\xa0\ \x1a\x04\x90\xb5\x18\x01\x12'\n\x1cMessageType_BinanceCancelMsg\x10\xc4\
\x06\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_WebAuthnCredentials\ \x05\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\
\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType_WebAuthnAddResid\ \xc5\x05\x1a\x04\x98\xb5\x18\x01\x126\n+MessageType_WebAuthnListResident\
entCredential\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x127\n,MessageType_Web\ Credentials\x10\xa0\x06\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_We\
AuthnRemoveResidentCredential\x10\xa3\x06\x1a\x04\x90\xb5\x18\x01\x12)\n\ bAuthnCredentials\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x124\n)MessageType\
\x1eMessageType_SolanaGetPublicKey\x10\x84\x07\x1a\x04\x90\xb5\x18\x01\ _WebAuthnAddResidentCredential\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x127\
\x12&\n\x1bMessageType_SolanaPublicKey\x10\x85\x07\x1a\x04\x98\xb5\x18\ \n,MessageType_WebAuthnRemoveResidentCredential\x10\xa3\x06\x1a\x04\x90\
\x01\x12'\n\x1cMessageType_SolanaGetAddress\x10\x86\x07\x1a\x04\x90\xb5\ \xb5\x18\x01\x12)\n\x1eMessageType_SolanaGetPublicKey\x10\x84\x07\x1a\
\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\x87\x07\x1a\x04\x98\xb5\ \x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_SolanaPublicKey\x10\x85\x07\
\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\x88\x07\x1a\x04\x90\xb5\ \x1a\x04\x98\xb5\x18\x01\x12'\n\x1cMessageType_SolanaGetAddress\x10\x86\
\x18\x01\x12(\n\x1dMessageType_SolanaTxSignature\x10\x89\x07\x1a\x04\x98\ \x07\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\x87\
\xb5\x18\x01\x12)\n\x1eMessageType_BenchmarkListNames\x10\x8cG\x1a\x04\ \x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\x88\
\x80\xa6\x1d\x01\x12%\n\x1aMessageType_BenchmarkNames\x10\x8dG\x1a\x04\ \x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_SolanaTxSignature\x10\
\x80\xa6\x1d\x01\x12#\n\x18MessageType_BenchmarkRun\x10\x8eG\x1a\x04\x80\ \x89\x07\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BenchmarkListName\
\xa6\x1d\x01\x12&\n\x1bMessageType_BenchmarkResult\x10\x8fG\x1a\x04\x80\ s\x10\x8cG\x1a\x04\x80\xa6\x1d\x01\x12%\n\x1aMessageType_BenchmarkNames\
\xa6\x1d\x01\x1a\x04\xc8\xf3\x18\x01\"\x04\x08Z\x10\\\"\x04\x08G\x10J\"\ \x10\x8dG\x1a\x04\x80\xa6\x1d\x01\x12#\n\x18MessageType_BenchmarkRun\x10\
\x04\x08r\x10z\"\x06\x08\xdb\x01\x10\xdb\x01\"\x06\x08\xe0\x01\x10\xe0\ \x8eG\x1a\x04\x80\xa6\x1d\x01\x12&\n\x1bMessageType_BenchmarkResult\x10\
\x01\"\x06\x08\xac\x02\x10\xb0\x02\"\x06\x08\xb5\x02\x10\xb8\x02\"\x06\ \x8fG\x1a\x04\x80\xa6\x1d\x01\x1a\x04\xc8\xf3\x18\x01\"\x04\x08Z\x10\\\"\
\x08\xe8\x07\x10\xcb\x08B8\n#com.satoshilabs.trezor.lib.protobufB\rTrezo\ \x04\x08G\x10J\"\x04\x08r\x10z\"\x06\x08\xdb\x01\x10\xdb\x01\"\x06\x08\
rMessage\x80\xa6\x1d\x01\ \xe0\x01\x10\xe0\x01\"\x06\x08\xac\x02\x10\xb0\x02\"\x06\x08\xb5\x02\x10\
\xb8\x02\"\x06\x08\xe8\x07\x10\xcb\x08B8\n#com.satoshilabs.trezor.lib.pr\
otobufB\rTrezorMessage\x80\xa6\x1d\x01\
"; ";
/// `FileDescriptorProto` object which was a source for this generated file /// `FileDescriptorProto` object which was a source for this generated file

View File

@ -414,6 +414,14 @@ pub mod failure {
Failure_WipeCodeMismatch = 13, Failure_WipeCodeMismatch = 13,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_InvalidSession) // @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_InvalidSession)
Failure_InvalidSession = 14, Failure_InvalidSession = 14,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_ThpUnallocatedSession)
Failure_ThpUnallocatedSession = 15,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_InvalidProtocol)
Failure_InvalidProtocol = 16,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_BufferError)
Failure_BufferError = 17,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_DeviceIsBusy)
Failure_DeviceIsBusy = 18,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_FirmwareError) // @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_FirmwareError)
Failure_FirmwareError = 99, Failure_FirmwareError = 99,
} }
@ -441,6 +449,10 @@ pub mod failure {
12 => ::std::option::Option::Some(FailureType::Failure_PinMismatch), 12 => ::std::option::Option::Some(FailureType::Failure_PinMismatch),
13 => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch), 13 => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch),
14 => ::std::option::Option::Some(FailureType::Failure_InvalidSession), 14 => ::std::option::Option::Some(FailureType::Failure_InvalidSession),
15 => ::std::option::Option::Some(FailureType::Failure_ThpUnallocatedSession),
16 => ::std::option::Option::Some(FailureType::Failure_InvalidProtocol),
17 => ::std::option::Option::Some(FailureType::Failure_BufferError),
18 => ::std::option::Option::Some(FailureType::Failure_DeviceIsBusy),
99 => ::std::option::Option::Some(FailureType::Failure_FirmwareError), 99 => ::std::option::Option::Some(FailureType::Failure_FirmwareError),
_ => ::std::option::Option::None _ => ::std::option::Option::None
} }
@ -462,6 +474,10 @@ pub mod failure {
"Failure_PinMismatch" => ::std::option::Option::Some(FailureType::Failure_PinMismatch), "Failure_PinMismatch" => ::std::option::Option::Some(FailureType::Failure_PinMismatch),
"Failure_WipeCodeMismatch" => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch), "Failure_WipeCodeMismatch" => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch),
"Failure_InvalidSession" => ::std::option::Option::Some(FailureType::Failure_InvalidSession), "Failure_InvalidSession" => ::std::option::Option::Some(FailureType::Failure_InvalidSession),
"Failure_ThpUnallocatedSession" => ::std::option::Option::Some(FailureType::Failure_ThpUnallocatedSession),
"Failure_InvalidProtocol" => ::std::option::Option::Some(FailureType::Failure_InvalidProtocol),
"Failure_BufferError" => ::std::option::Option::Some(FailureType::Failure_BufferError),
"Failure_DeviceIsBusy" => ::std::option::Option::Some(FailureType::Failure_DeviceIsBusy),
"Failure_FirmwareError" => ::std::option::Option::Some(FailureType::Failure_FirmwareError), "Failure_FirmwareError" => ::std::option::Option::Some(FailureType::Failure_FirmwareError),
_ => ::std::option::Option::None _ => ::std::option::Option::None
} }
@ -482,6 +498,10 @@ pub mod failure {
FailureType::Failure_PinMismatch, FailureType::Failure_PinMismatch,
FailureType::Failure_WipeCodeMismatch, FailureType::Failure_WipeCodeMismatch,
FailureType::Failure_InvalidSession, FailureType::Failure_InvalidSession,
FailureType::Failure_ThpUnallocatedSession,
FailureType::Failure_InvalidProtocol,
FailureType::Failure_BufferError,
FailureType::Failure_DeviceIsBusy,
FailureType::Failure_FirmwareError, FailureType::Failure_FirmwareError,
]; ];
} }
@ -508,7 +528,11 @@ pub mod failure {
FailureType::Failure_PinMismatch => 11, FailureType::Failure_PinMismatch => 11,
FailureType::Failure_WipeCodeMismatch => 12, FailureType::Failure_WipeCodeMismatch => 12,
FailureType::Failure_InvalidSession => 13, FailureType::Failure_InvalidSession => 13,
FailureType::Failure_FirmwareError => 14, FailureType::Failure_ThpUnallocatedSession => 14,
FailureType::Failure_InvalidProtocol => 15,
FailureType::Failure_BufferError => 16,
FailureType::Failure_DeviceIsBusy => 17,
FailureType::Failure_FirmwareError => 18,
}; };
Self::enum_descriptor().value_by_index(index) Self::enum_descriptor().value_by_index(index)
} }
@ -2481,9 +2505,9 @@ impl ::protobuf::reflect::ProtobufValue for HDNodeType {
static file_descriptor_proto_data: &'static [u8] = b"\ static file_descriptor_proto_data: &'static [u8] = b"\
\n\x15messages-common.proto\x12\x19hw.trezor.messages.common\x1a\roption\ \n\x15messages-common.proto\x12\x19hw.trezor.messages.common\x1a\roption\
s.proto\"%\n\x07Success\x12\x1a\n\x07message\x18\x01\x20\x01(\t:\0R\x07m\ s.proto\"%\n\x07Success\x12\x1a\n\x07message\x18\x01\x20\x01(\t:\0R\x07m\
essage\"\x8f\x04\n\x07Failure\x12B\n\x04code\x18\x01\x20\x01(\x0e2..hw.t\ essage\"\x82\x05\n\x07Failure\x12B\n\x04code\x18\x01\x20\x01(\x0e2..hw.t\
rezor.messages.common.Failure.FailureTypeR\x04code\x12\x18\n\x07message\ rezor.messages.common.Failure.FailureTypeR\x04code\x12\x18\n\x07message\
\x18\x02\x20\x01(\tR\x07message\"\xa5\x03\n\x0bFailureType\x12\x1d\n\x19\ \x18\x02\x20\x01(\tR\x07message\"\x98\x04\n\x0bFailureType\x12\x1d\n\x19\
Failure_UnexpectedMessage\x10\x01\x12\x1a\n\x16Failure_ButtonExpected\ Failure_UnexpectedMessage\x10\x01\x12\x1a\n\x16Failure_ButtonExpected\
\x10\x02\x12\x15\n\x11Failure_DataError\x10\x03\x12\x1b\n\x17Failure_Act\ \x10\x02\x12\x15\n\x11Failure_DataError\x10\x03\x12\x1b\n\x17Failure_Act\
ionCancelled\x10\x04\x12\x17\n\x13Failure_PinExpected\x10\x05\x12\x18\n\ ionCancelled\x10\x04\x12\x17\n\x13Failure_PinExpected\x10\x05\x12\x18\n\
@ -2492,44 +2516,46 @@ static file_descriptor_proto_data: &'static [u8] = b"\
essError\x10\t\x12\x1a\n\x16Failure_NotEnoughFunds\x10\n\x12\x1a\n\x16Fa\ essError\x10\t\x12\x1a\n\x16Failure_NotEnoughFunds\x10\n\x12\x1a\n\x16Fa\
ilure_NotInitialized\x10\x0b\x12\x17\n\x13Failure_PinMismatch\x10\x0c\ ilure_NotInitialized\x10\x0b\x12\x17\n\x13Failure_PinMismatch\x10\x0c\
\x12\x1c\n\x18Failure_WipeCodeMismatch\x10\r\x12\x1a\n\x16Failure_Invali\ \x12\x1c\n\x18Failure_WipeCodeMismatch\x10\r\x12\x1a\n\x16Failure_Invali\
dSession\x10\x0e\x12\x19\n\x15Failure_FirmwareError\x10c\"\xab\x06\n\rBu\ dSession\x10\x0e\x12!\n\x1dFailure_ThpUnallocatedSession\x10\x0f\x12\x1b\
ttonRequest\x12N\n\x04code\x18\x01\x20\x01(\x0e2:.hw.trezor.messages.com\ \n\x17Failure_InvalidProtocol\x10\x10\x12\x17\n\x13Failure_BufferError\
mon.ButtonRequest.ButtonRequestTypeR\x04code\x12\x14\n\x05pages\x18\x02\ \x10\x11\x12\x18\n\x14Failure_DeviceIsBusy\x10\x12\x12\x19\n\x15Failure_\
\x20\x01(\rR\x05pages\x12\x12\n\x04name\x18\x04\x20\x01(\tR\x04name\"\ FirmwareError\x10c\"\xab\x06\n\rButtonRequest\x12N\n\x04code\x18\x01\x20\
\x99\x05\n\x11ButtonRequestType\x12\x17\n\x13ButtonRequest_Other\x10\x01\ \x01(\x0e2:.hw.trezor.messages.common.ButtonRequest.ButtonRequestTypeR\
\x12\"\n\x1eButtonRequest_FeeOverThreshold\x10\x02\x12\x1f\n\x1bButtonRe\ \x04code\x12\x14\n\x05pages\x18\x02\x20\x01(\rR\x05pages\x12\x12\n\x04na\
quest_ConfirmOutput\x10\x03\x12\x1d\n\x19ButtonRequest_ResetDevice\x10\ me\x18\x04\x20\x01(\tR\x04name\"\x99\x05\n\x11ButtonRequestType\x12\x17\
\x04\x12\x1d\n\x19ButtonRequest_ConfirmWord\x10\x05\x12\x1c\n\x18ButtonR\ \n\x13ButtonRequest_Other\x10\x01\x12\"\n\x1eButtonRequest_FeeOverThresh\
equest_WipeDevice\x10\x06\x12\x1d\n\x19ButtonRequest_ProtectCall\x10\x07\ old\x10\x02\x12\x1f\n\x1bButtonRequest_ConfirmOutput\x10\x03\x12\x1d\n\
\x12\x18\n\x14ButtonRequest_SignTx\x10\x08\x12\x1f\n\x1bButtonRequest_Fi\ \x19ButtonRequest_ResetDevice\x10\x04\x12\x1d\n\x19ButtonRequest_Confirm\
rmwareCheck\x10\t\x12\x19\n\x15ButtonRequest_Address\x10\n\x12\x1b\n\x17\ Word\x10\x05\x12\x1c\n\x18ButtonRequest_WipeDevice\x10\x06\x12\x1d\n\x19\
ButtonRequest_PublicKey\x10\x0b\x12#\n\x1fButtonRequest_MnemonicWordCoun\ ButtonRequest_ProtectCall\x10\x07\x12\x18\n\x14ButtonRequest_SignTx\x10\
t\x10\x0c\x12\x1f\n\x1bButtonRequest_MnemonicInput\x10\r\x120\n(_Depreca\ \x08\x12\x1f\n\x1bButtonRequest_FirmwareCheck\x10\t\x12\x19\n\x15ButtonR\
ted_ButtonRequest_PassphraseType\x10\x0e\x1a\x02\x08\x01\x12'\n#ButtonRe\ equest_Address\x10\n\x12\x1b\n\x17ButtonRequest_PublicKey\x10\x0b\x12#\n\
quest_UnknownDerivationPath\x10\x0f\x12\"\n\x1eButtonRequest_RecoveryHom\ \x1fButtonRequest_MnemonicWordCount\x10\x0c\x12\x1f\n\x1bButtonRequest_M\
epage\x10\x10\x12\x19\n\x15ButtonRequest_Success\x10\x11\x12\x19\n\x15Bu\ nemonicInput\x10\r\x120\n(_Deprecated_ButtonRequest_PassphraseType\x10\
ttonRequest_Warning\x10\x12\x12!\n\x1dButtonRequest_PassphraseEntry\x10\ \x0e\x1a\x02\x08\x01\x12'\n#ButtonRequest_UnknownDerivationPath\x10\x0f\
\x13\x12\x1a\n\x16ButtonRequest_PinEntry\x10\x14J\x04\x08\x03\x10\x04\"\ \x12\"\n\x1eButtonRequest_RecoveryHomepage\x10\x10\x12\x19\n\x15ButtonRe\
\x0b\n\tButtonAck\"\xbb\x02\n\x10PinMatrixRequest\x12T\n\x04type\x18\x01\ quest_Success\x10\x11\x12\x19\n\x15ButtonRequest_Warning\x10\x12\x12!\n\
\x20\x01(\x0e2@.hw.trezor.messages.common.PinMatrixRequest.PinMatrixRequ\ \x1dButtonRequest_PassphraseEntry\x10\x13\x12\x1a\n\x16ButtonRequest_Pin\
estTypeR\x04type\"\xd0\x01\n\x14PinMatrixRequestType\x12\x20\n\x1cPinMat\ Entry\x10\x14J\x04\x08\x03\x10\x04\"\x0b\n\tButtonAck\"\xbb\x02\n\x10Pin\
rixRequestType_Current\x10\x01\x12!\n\x1dPinMatrixRequestType_NewFirst\ MatrixRequest\x12T\n\x04type\x18\x01\x20\x01(\x0e2@.hw.trezor.messages.c\
\x10\x02\x12\"\n\x1ePinMatrixRequestType_NewSecond\x10\x03\x12&\n\"PinMa\ ommon.PinMatrixRequest.PinMatrixRequestTypeR\x04type\"\xd0\x01\n\x14PinM\
trixRequestType_WipeCodeFirst\x10\x04\x12'\n#PinMatrixRequestType_WipeCo\ atrixRequestType\x12\x20\n\x1cPinMatrixRequestType_Current\x10\x01\x12!\
deSecond\x10\x05\"\x20\n\x0cPinMatrixAck\x12\x10\n\x03pin\x18\x01\x20\ \n\x1dPinMatrixRequestType_NewFirst\x10\x02\x12\"\n\x1ePinMatrixRequestT\
\x02(\tR\x03pin\"5\n\x11PassphraseRequest\x12\x20\n\n_on_device\x18\x01\ ype_NewSecond\x10\x03\x12&\n\"PinMatrixRequestType_WipeCodeFirst\x10\x04\
\x20\x01(\x08R\x08OnDeviceB\x02\x18\x01\"g\n\rPassphraseAck\x12\x1e\n\np\ \x12'\n#PinMatrixRequestType_WipeCodeSecond\x10\x05\"\x20\n\x0cPinMatrix\
assphrase\x18\x01\x20\x01(\tR\npassphrase\x12\x19\n\x06_state\x18\x02\ Ack\x12\x10\n\x03pin\x18\x01\x20\x02(\tR\x03pin\"5\n\x11PassphraseReques\
\x20\x01(\x0cR\x05StateB\x02\x18\x01\x12\x1b\n\ton_device\x18\x03\x20\ t\x12\x20\n\n_on_device\x18\x01\x20\x01(\x08R\x08OnDeviceB\x02\x18\x01\"\
\x01(\x08R\x08onDevice\"=\n!Deprecated_PassphraseStateRequest\x12\x14\n\ g\n\rPassphraseAck\x12\x1e\n\npassphrase\x18\x01\x20\x01(\tR\npassphrase\
\x05state\x18\x01\x20\x01(\x0cR\x05state:\x02\x18\x01\"#\n\x1dDeprecated\ \x12\x19\n\x06_state\x18\x02\x20\x01(\x0cR\x05StateB\x02\x18\x01\x12\x1b\
_PassphraseStateAck:\x02\x18\x01\"\xc0\x01\n\nHDNodeType\x12\x14\n\x05de\ \n\ton_device\x18\x03\x20\x01(\x08R\x08onDevice\"=\n!Deprecated_Passphra\
pth\x18\x01\x20\x02(\rR\x05depth\x12\x20\n\x0bfingerprint\x18\x02\x20\ seStateRequest\x12\x14\n\x05state\x18\x01\x20\x01(\x0cR\x05state:\x02\
\x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\x18\x03\x20\x02(\rR\x08chil\ \x18\x01\"#\n\x1dDeprecated_PassphraseStateAck:\x02\x18\x01\"\xc0\x01\n\
dNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\x0cR\tchainCode\x12\x1f\n\ \nHDNodeType\x12\x14\n\x05depth\x18\x01\x20\x02(\rR\x05depth\x12\x20\n\
\x0bprivate_key\x18\x05\x20\x01(\x0cR\nprivateKey\x12\x1d\n\npublic_key\ \x0bfingerprint\x18\x02\x20\x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\
\x18\x06\x20\x02(\x0cR\tpublicKeyB>\n#com.satoshilabs.trezor.lib.protobu\ \x18\x03\x20\x02(\rR\x08childNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\
fB\x13TrezorMessageCommon\x80\xa6\x1d\x01\ \x0cR\tchainCode\x12\x1f\n\x0bprivate_key\x18\x05\x20\x01(\x0cR\nprivate\
Key\x12\x1d\n\npublic_key\x18\x06\x20\x02(\x0cR\tpublicKeyB>\n#com.satos\
hilabs.trezor.lib.protobufB\x13TrezorMessageCommon\x80\xa6\x1d\x01\
"; ";
/// `FileDescriptorProto` object which was a source for this generated file /// `FileDescriptorProto` object which was a source for this generated file

View File

@ -2118,6 +2118,629 @@ impl ::protobuf::reflect::ProtobufValue for DebugLinkState {
type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>; type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
} }
// @@protoc_insertion_point(message:hw.trezor.messages.debug.DebugLinkGetPairingInfo)
#[derive(PartialEq,Clone,Default,Debug)]
pub struct DebugLinkGetPairingInfo {
// message fields
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetPairingInfo.channel_id)
pub channel_id: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetPairingInfo.handshake_hash)
pub handshake_hash: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetPairingInfo.nfc_secret_host)
pub nfc_secret_host: ::std::option::Option<::std::vec::Vec<u8>>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.debug.DebugLinkGetPairingInfo.special_fields)
pub special_fields: ::protobuf::SpecialFields,
}
impl<'a> ::std::default::Default for &'a DebugLinkGetPairingInfo {
fn default() -> &'a DebugLinkGetPairingInfo {
<DebugLinkGetPairingInfo as ::protobuf::Message>::default_instance()
}
}
impl DebugLinkGetPairingInfo {
pub fn new() -> DebugLinkGetPairingInfo {
::std::default::Default::default()
}
// optional bytes channel_id = 1;
pub fn channel_id(&self) -> &[u8] {
match self.channel_id.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_channel_id(&mut self) {
self.channel_id = ::std::option::Option::None;
}
pub fn has_channel_id(&self) -> bool {
self.channel_id.is_some()
}
// Param is passed by value, moved
pub fn set_channel_id(&mut self, v: ::std::vec::Vec<u8>) {
self.channel_id = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_channel_id(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.channel_id.is_none() {
self.channel_id = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.channel_id.as_mut().unwrap()
}
// Take field
pub fn take_channel_id(&mut self) -> ::std::vec::Vec<u8> {
self.channel_id.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// optional bytes handshake_hash = 2;
pub fn handshake_hash(&self) -> &[u8] {
match self.handshake_hash.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_handshake_hash(&mut self) {
self.handshake_hash = ::std::option::Option::None;
}
pub fn has_handshake_hash(&self) -> bool {
self.handshake_hash.is_some()
}
// Param is passed by value, moved
pub fn set_handshake_hash(&mut self, v: ::std::vec::Vec<u8>) {
self.handshake_hash = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_handshake_hash(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.handshake_hash.is_none() {
self.handshake_hash = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.handshake_hash.as_mut().unwrap()
}
// Take field
pub fn take_handshake_hash(&mut self) -> ::std::vec::Vec<u8> {
self.handshake_hash.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// optional bytes nfc_secret_host = 3;
pub fn nfc_secret_host(&self) -> &[u8] {
match self.nfc_secret_host.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_nfc_secret_host(&mut self) {
self.nfc_secret_host = ::std::option::Option::None;
}
pub fn has_nfc_secret_host(&self) -> bool {
self.nfc_secret_host.is_some()
}
// Param is passed by value, moved
pub fn set_nfc_secret_host(&mut self, v: ::std::vec::Vec<u8>) {
self.nfc_secret_host = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_nfc_secret_host(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.nfc_secret_host.is_none() {
self.nfc_secret_host = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.nfc_secret_host.as_mut().unwrap()
}
// Take field
pub fn take_nfc_secret_host(&mut self) -> ::std::vec::Vec<u8> {
self.nfc_secret_host.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
let mut fields = ::std::vec::Vec::with_capacity(3);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"channel_id",
|m: &DebugLinkGetPairingInfo| { &m.channel_id },
|m: &mut DebugLinkGetPairingInfo| { &mut m.channel_id },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"handshake_hash",
|m: &DebugLinkGetPairingInfo| { &m.handshake_hash },
|m: &mut DebugLinkGetPairingInfo| { &mut m.handshake_hash },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"nfc_secret_host",
|m: &DebugLinkGetPairingInfo| { &m.nfc_secret_host },
|m: &mut DebugLinkGetPairingInfo| { &mut m.nfc_secret_host },
));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<DebugLinkGetPairingInfo>(
"DebugLinkGetPairingInfo",
fields,
oneofs,
)
}
}
impl ::protobuf::Message for DebugLinkGetPairingInfo {
const NAME: &'static str = "DebugLinkGetPairingInfo";
fn is_initialized(&self) -> bool {
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> {
while let Some(tag) = is.read_raw_tag_or_eof()? {
match tag {
10 => {
self.channel_id = ::std::option::Option::Some(is.read_bytes()?);
},
18 => {
self.handshake_hash = ::std::option::Option::Some(is.read_bytes()?);
},
26 => {
self.nfc_secret_host = ::std::option::Option::Some(is.read_bytes()?);
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u64 {
let mut my_size = 0;
if let Some(v) = self.channel_id.as_ref() {
my_size += ::protobuf::rt::bytes_size(1, &v);
}
if let Some(v) = self.handshake_hash.as_ref() {
my_size += ::protobuf::rt::bytes_size(2, &v);
}
if let Some(v) = self.nfc_secret_host.as_ref() {
my_size += ::protobuf::rt::bytes_size(3, &v);
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> {
if let Some(v) = self.channel_id.as_ref() {
os.write_bytes(1, v)?;
}
if let Some(v) = self.handshake_hash.as_ref() {
os.write_bytes(2, v)?;
}
if let Some(v) = self.nfc_secret_host.as_ref() {
os.write_bytes(3, v)?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
fn special_fields(&self) -> &::protobuf::SpecialFields {
&self.special_fields
}
fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields {
&mut self.special_fields
}
fn new() -> DebugLinkGetPairingInfo {
DebugLinkGetPairingInfo::new()
}
fn clear(&mut self) {
self.channel_id = ::std::option::Option::None;
self.handshake_hash = ::std::option::Option::None;
self.nfc_secret_host = ::std::option::Option::None;
self.special_fields.clear();
}
fn default_instance() -> &'static DebugLinkGetPairingInfo {
static instance: DebugLinkGetPairingInfo = DebugLinkGetPairingInfo {
channel_id: ::std::option::Option::None,
handshake_hash: ::std::option::Option::None,
nfc_secret_host: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
}
}
impl ::protobuf::MessageFull for DebugLinkGetPairingInfo {
fn descriptor() -> ::protobuf::reflect::MessageDescriptor {
static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new();
descriptor.get(|| file_descriptor().message_by_package_relative_name("DebugLinkGetPairingInfo").unwrap()).clone()
}
}
impl ::std::fmt::Display for DebugLinkGetPairingInfo {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::protobuf::text_format::fmt(self, f)
}
}
impl ::protobuf::reflect::ProtobufValue for DebugLinkGetPairingInfo {
type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
}
// @@protoc_insertion_point(message:hw.trezor.messages.debug.DebugLinkPairingInfo)
#[derive(PartialEq,Clone,Default,Debug)]
pub struct DebugLinkPairingInfo {
// message fields
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkPairingInfo.channel_id)
pub channel_id: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkPairingInfo.handshake_hash)
pub handshake_hash: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkPairingInfo.code_entry_code)
pub code_entry_code: ::std::option::Option<u32>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkPairingInfo.code_qr_code)
pub code_qr_code: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkPairingInfo.nfc_secret_trezor)
pub nfc_secret_trezor: ::std::option::Option<::std::vec::Vec<u8>>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.debug.DebugLinkPairingInfo.special_fields)
pub special_fields: ::protobuf::SpecialFields,
}
impl<'a> ::std::default::Default for &'a DebugLinkPairingInfo {
fn default() -> &'a DebugLinkPairingInfo {
<DebugLinkPairingInfo as ::protobuf::Message>::default_instance()
}
}
impl DebugLinkPairingInfo {
pub fn new() -> DebugLinkPairingInfo {
::std::default::Default::default()
}
// optional bytes channel_id = 1;
pub fn channel_id(&self) -> &[u8] {
match self.channel_id.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_channel_id(&mut self) {
self.channel_id = ::std::option::Option::None;
}
pub fn has_channel_id(&self) -> bool {
self.channel_id.is_some()
}
// Param is passed by value, moved
pub fn set_channel_id(&mut self, v: ::std::vec::Vec<u8>) {
self.channel_id = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_channel_id(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.channel_id.is_none() {
self.channel_id = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.channel_id.as_mut().unwrap()
}
// Take field
pub fn take_channel_id(&mut self) -> ::std::vec::Vec<u8> {
self.channel_id.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// optional bytes handshake_hash = 2;
pub fn handshake_hash(&self) -> &[u8] {
match self.handshake_hash.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_handshake_hash(&mut self) {
self.handshake_hash = ::std::option::Option::None;
}
pub fn has_handshake_hash(&self) -> bool {
self.handshake_hash.is_some()
}
// Param is passed by value, moved
pub fn set_handshake_hash(&mut self, v: ::std::vec::Vec<u8>) {
self.handshake_hash = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_handshake_hash(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.handshake_hash.is_none() {
self.handshake_hash = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.handshake_hash.as_mut().unwrap()
}
// Take field
pub fn take_handshake_hash(&mut self) -> ::std::vec::Vec<u8> {
self.handshake_hash.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// optional uint32 code_entry_code = 3;
pub fn code_entry_code(&self) -> u32 {
self.code_entry_code.unwrap_or(0)
}
pub fn clear_code_entry_code(&mut self) {
self.code_entry_code = ::std::option::Option::None;
}
pub fn has_code_entry_code(&self) -> bool {
self.code_entry_code.is_some()
}
// Param is passed by value, moved
pub fn set_code_entry_code(&mut self, v: u32) {
self.code_entry_code = ::std::option::Option::Some(v);
}
// optional bytes code_qr_code = 4;
pub fn code_qr_code(&self) -> &[u8] {
match self.code_qr_code.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_code_qr_code(&mut self) {
self.code_qr_code = ::std::option::Option::None;
}
pub fn has_code_qr_code(&self) -> bool {
self.code_qr_code.is_some()
}
// Param is passed by value, moved
pub fn set_code_qr_code(&mut self, v: ::std::vec::Vec<u8>) {
self.code_qr_code = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_code_qr_code(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.code_qr_code.is_none() {
self.code_qr_code = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.code_qr_code.as_mut().unwrap()
}
// Take field
pub fn take_code_qr_code(&mut self) -> ::std::vec::Vec<u8> {
self.code_qr_code.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// optional bytes nfc_secret_trezor = 5;
pub fn nfc_secret_trezor(&self) -> &[u8] {
match self.nfc_secret_trezor.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_nfc_secret_trezor(&mut self) {
self.nfc_secret_trezor = ::std::option::Option::None;
}
pub fn has_nfc_secret_trezor(&self) -> bool {
self.nfc_secret_trezor.is_some()
}
// Param is passed by value, moved
pub fn set_nfc_secret_trezor(&mut self, v: ::std::vec::Vec<u8>) {
self.nfc_secret_trezor = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_nfc_secret_trezor(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.nfc_secret_trezor.is_none() {
self.nfc_secret_trezor = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.nfc_secret_trezor.as_mut().unwrap()
}
// Take field
pub fn take_nfc_secret_trezor(&mut self) -> ::std::vec::Vec<u8> {
self.nfc_secret_trezor.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
let mut fields = ::std::vec::Vec::with_capacity(5);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"channel_id",
|m: &DebugLinkPairingInfo| { &m.channel_id },
|m: &mut DebugLinkPairingInfo| { &mut m.channel_id },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"handshake_hash",
|m: &DebugLinkPairingInfo| { &m.handshake_hash },
|m: &mut DebugLinkPairingInfo| { &mut m.handshake_hash },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"code_entry_code",
|m: &DebugLinkPairingInfo| { &m.code_entry_code },
|m: &mut DebugLinkPairingInfo| { &mut m.code_entry_code },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"code_qr_code",
|m: &DebugLinkPairingInfo| { &m.code_qr_code },
|m: &mut DebugLinkPairingInfo| { &mut m.code_qr_code },
));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"nfc_secret_trezor",
|m: &DebugLinkPairingInfo| { &m.nfc_secret_trezor },
|m: &mut DebugLinkPairingInfo| { &mut m.nfc_secret_trezor },
));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<DebugLinkPairingInfo>(
"DebugLinkPairingInfo",
fields,
oneofs,
)
}
}
impl ::protobuf::Message for DebugLinkPairingInfo {
const NAME: &'static str = "DebugLinkPairingInfo";
fn is_initialized(&self) -> bool {
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> {
while let Some(tag) = is.read_raw_tag_or_eof()? {
match tag {
10 => {
self.channel_id = ::std::option::Option::Some(is.read_bytes()?);
},
18 => {
self.handshake_hash = ::std::option::Option::Some(is.read_bytes()?);
},
24 => {
self.code_entry_code = ::std::option::Option::Some(is.read_uint32()?);
},
34 => {
self.code_qr_code = ::std::option::Option::Some(is.read_bytes()?);
},
42 => {
self.nfc_secret_trezor = ::std::option::Option::Some(is.read_bytes()?);
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u64 {
let mut my_size = 0;
if let Some(v) = self.channel_id.as_ref() {
my_size += ::protobuf::rt::bytes_size(1, &v);
}
if let Some(v) = self.handshake_hash.as_ref() {
my_size += ::protobuf::rt::bytes_size(2, &v);
}
if let Some(v) = self.code_entry_code {
my_size += ::protobuf::rt::uint32_size(3, v);
}
if let Some(v) = self.code_qr_code.as_ref() {
my_size += ::protobuf::rt::bytes_size(4, &v);
}
if let Some(v) = self.nfc_secret_trezor.as_ref() {
my_size += ::protobuf::rt::bytes_size(5, &v);
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> {
if let Some(v) = self.channel_id.as_ref() {
os.write_bytes(1, v)?;
}
if let Some(v) = self.handshake_hash.as_ref() {
os.write_bytes(2, v)?;
}
if let Some(v) = self.code_entry_code {
os.write_uint32(3, v)?;
}
if let Some(v) = self.code_qr_code.as_ref() {
os.write_bytes(4, v)?;
}
if let Some(v) = self.nfc_secret_trezor.as_ref() {
os.write_bytes(5, v)?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
fn special_fields(&self) -> &::protobuf::SpecialFields {
&self.special_fields
}
fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields {
&mut self.special_fields
}
fn new() -> DebugLinkPairingInfo {
DebugLinkPairingInfo::new()
}
fn clear(&mut self) {
self.channel_id = ::std::option::Option::None;
self.handshake_hash = ::std::option::Option::None;
self.code_entry_code = ::std::option::Option::None;
self.code_qr_code = ::std::option::Option::None;
self.nfc_secret_trezor = ::std::option::Option::None;
self.special_fields.clear();
}
fn default_instance() -> &'static DebugLinkPairingInfo {
static instance: DebugLinkPairingInfo = DebugLinkPairingInfo {
channel_id: ::std::option::Option::None,
handshake_hash: ::std::option::Option::None,
code_entry_code: ::std::option::Option::None,
code_qr_code: ::std::option::Option::None,
nfc_secret_trezor: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
}
}
impl ::protobuf::MessageFull for DebugLinkPairingInfo {
fn descriptor() -> ::protobuf::reflect::MessageDescriptor {
static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new();
descriptor.get(|| file_descriptor().message_by_package_relative_name("DebugLinkPairingInfo").unwrap()).clone()
}
}
impl ::std::fmt::Display for DebugLinkPairingInfo {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::protobuf::text_format::fmt(self, f)
}
}
impl ::protobuf::reflect::ProtobufValue for DebugLinkPairingInfo {
type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
}
// @@protoc_insertion_point(message:hw.trezor.messages.debug.DebugLinkStop) // @@protoc_insertion_point(message:hw.trezor.messages.debug.DebugLinkStop)
#[derive(PartialEq,Clone,Default,Debug)] #[derive(PartialEq,Clone,Default,Debug)]
pub struct DebugLinkStop { pub struct DebugLinkStop {
@ -3707,20 +4330,28 @@ static file_descriptor_proto_data: &'static [u8] = b"\
dPos\x12$\n\x0ereset_word_pos\x18\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\ dPos\x12$\n\x0ereset_word_pos\x18\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\
\rmnemonic_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.Bac\ \rmnemonic_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.Bac\
kupTypeR\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\ kupTypeR\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\
\"\x0f\n\rDebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\ \"\x87\x01\n\x17DebugLinkGetPairingInfo\x12\x1d\n\nchannel_id\x18\x01\
\x20\x01(\rR\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\ \x20\x01(\x0cR\tchannelId\x12%\n\x0ehandshake_hash\x18\x02\x20\x01(\x0cR\
\x12\x12\n\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRe\ \rhandshakeHash\x12&\n\x0fnfc_secret_host\x18\x03\x20\x01(\x0cR\rnfcSecr\
ad\x12\x18\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06leng\ etHost\"\xd2\x01\n\x14DebugLinkPairingInfo\x12\x1d\n\nchannel_id\x18\x01\
th\x18\x02\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06me\ \x20\x01(\x0cR\tchannelId\x12%\n\x0ehandshake_hash\x18\x02\x20\x01(\x0cR\
mory\x18\x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\ \rhandshakeHash\x12&\n\x0fcode_entry_code\x18\x03\x20\x01(\rR\rcodeEntry\
\x18\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06memory\x18\ Code\x12\x20\n\x0ccode_qr_code\x18\x04\x20\x01(\x0cR\ncodeQrCode\x12*\n\
\x02\x20\x01(\x0cR\x06memory\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\ \x11nfc_secret_trezor\x18\x05\x20\x01(\x0cR\x0fnfcSecretTrezor\"\x0f\n\r\
\x05flash\"-\n\x13DebugLinkFlashErase\x12\x16\n\x06sector\x18\x01\x20\ DebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\x20\x01(\r\
\x01(\rR\x06sector\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\ R\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\x12\x12\n\
\x01\x20\x01(\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05w\ \x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRead\x12\x18\
atch\x18\x01\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkRes\ \n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06length\x18\x02\
etDebugEvents:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.s\ \x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06memory\x18\
atoshilabs.trezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\ \x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\x18\n\x07a\
ddress\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06memory\x18\x02\x20\
\x01(\x0cR\x06memory\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\x05flash\"\
-\n\x13DebugLinkFlashErase\x12\x16\n\x06sector\x18\x01\x20\x01(\rR\x06se\
ctor\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\x01\x20\x01(\
\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05watch\x18\x01\
\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkResetDebugEvent\
s:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.satoshilabs.t\
rezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\
"; ";
/// `FileDescriptorProto` object which was a source for this generated file /// `FileDescriptorProto` object which was a source for this generated file
@ -3741,13 +4372,15 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor {
deps.push(super::messages_common::file_descriptor().clone()); deps.push(super::messages_common::file_descriptor().clone());
deps.push(super::messages_management::file_descriptor().clone()); deps.push(super::messages_management::file_descriptor().clone());
deps.push(super::options::file_descriptor().clone()); deps.push(super::options::file_descriptor().clone());
let mut messages = ::std::vec::Vec::with_capacity(16); let mut messages = ::std::vec::Vec::with_capacity(18);
messages.push(DebugLinkDecision::generated_message_descriptor_data()); messages.push(DebugLinkDecision::generated_message_descriptor_data());
messages.push(DebugLinkLayout::generated_message_descriptor_data()); messages.push(DebugLinkLayout::generated_message_descriptor_data());
messages.push(DebugLinkReseedRandom::generated_message_descriptor_data()); messages.push(DebugLinkReseedRandom::generated_message_descriptor_data());
messages.push(DebugLinkRecordScreen::generated_message_descriptor_data()); messages.push(DebugLinkRecordScreen::generated_message_descriptor_data());
messages.push(DebugLinkGetState::generated_message_descriptor_data()); messages.push(DebugLinkGetState::generated_message_descriptor_data());
messages.push(DebugLinkState::generated_message_descriptor_data()); messages.push(DebugLinkState::generated_message_descriptor_data());
messages.push(DebugLinkGetPairingInfo::generated_message_descriptor_data());
messages.push(DebugLinkPairingInfo::generated_message_descriptor_data());
messages.push(DebugLinkStop::generated_message_descriptor_data()); messages.push(DebugLinkStop::generated_message_descriptor_data());
messages.push(DebugLinkLog::generated_message_descriptor_data()); messages.push(DebugLinkLog::generated_message_descriptor_data());
messages.push(DebugLinkMemoryRead::generated_message_descriptor_data()); messages.push(DebugLinkMemoryRead::generated_message_descriptor_data());

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,332 @@
import os
import random
import typing as t
from hashlib import sha256
import pytest
import typing_extensions as tx
from trezorlib import protobuf
from trezorlib.client import ProtocolV2
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.messages import (
ButtonAck,
ButtonRequest,
ThpCodeEntryChallenge,
ThpCodeEntryCommitment,
ThpCodeEntryCpaceHostTag,
ThpCodeEntryCpaceTrezor,
ThpCodeEntrySecret,
ThpCredentialRequest,
ThpCredentialResponse,
ThpEndRequest,
ThpEndResponse,
ThpNfcTagHost,
ThpNfcTagTrezor,
ThpPairingMethod,
ThpPairingPreparationsFinished,
ThpPairingRequest,
ThpPairingRequestApproved,
ThpQrCodeSecret,
ThpQrCodeTag,
ThpSelectMethod,
)
from trezorlib.transport.thp import curve25519
from trezorlib.transport.thp.cpace import Cpace
from trezorlib.transport.thp.protocol_v2 import _hkdf
if t.TYPE_CHECKING:
P = tx.ParamSpec("P")
MT = t.TypeVar("MT", bound=protobuf.MessageType)
pytestmark = [pytest.mark.protocol("protocol_v2")]
def _prepare_protocol(client: Client) -> ProtocolV2:
protocol = client.protocol
assert isinstance(protocol, ProtocolV2)
protocol._reset_sync_bits()
return protocol
def _prepare_protocol_for_pairing(client: Client) -> ProtocolV2:
protocol = _prepare_protocol(client)
protocol._do_channel_allocation()
protocol._do_handshake()
return protocol
def _handle_pairing_request(client: Client, protocol: ProtocolV2) -> None:
protocol._send_message(ThpPairingRequest())
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "pairing_request"
protocol._send_message(ButtonAck())
client.debug.press_yes()
protocol._read_message(ThpPairingRequestApproved)
def test_allocate_channel(client: Client) -> None:
protocol = _prepare_protocol(client)
nonce = random.randbytes(8)
# Use valid nonce
protocol._send_channel_allocation_request(nonce)
protocol._read_channel_allocation_response(nonce)
# Expect different nonce
protocol._send_channel_allocation_request(nonce)
with pytest.raises(Exception, match="Invalid channel allocation response."):
protocol._read_channel_allocation_response(
expected_nonce=b"\xDE\xAD\xBE\xEF\xDE\xAD\xBE\xEF"
)
client.invalidate()
def test_handshake(client: Client) -> None:
protocol = _prepare_protocol(client)
host_ephemeral_privkey = curve25519.get_private_key(os.urandom(32))
host_ephemeral_pubkey = curve25519.get_public_key(host_ephemeral_privkey)
protocol._do_channel_allocation()
protocol._send_handshake_init_request(host_ephemeral_pubkey)
protocol._read_ack()
init_response = protocol._read_handshake_init_response()
trezor_ephemeral_pubkey = init_response[:32]
encrypted_trezor_static_pubkey = init_response[32:80]
noise_tag = init_response[80:96]
# TODO check noise_tag is valid
ck = protocol._send_handshake_completion_request(
host_ephemeral_pubkey,
host_ephemeral_privkey,
trezor_ephemeral_pubkey,
encrypted_trezor_static_pubkey,
)
protocol._read_ack()
protocol._read_handshake_completion_response()
protocol.key_request, protocol.key_response = _hkdf(ck, b"")
protocol.nonce_request = 0
protocol.nonce_response = 1
# TODO - without pairing, the client is damaged and results in fail of the following test
# so far no luck in solving it - it should be also tackled in FW, as it causes unexpected FW error
protocol._do_pairing(client.debug)
# TODO the following is just to make style checker happy
assert noise_tag is not None
def test_pairing_qr_code(client: Client) -> None:
protocol = _prepare_protocol_for_pairing(client)
_handle_pairing_request(client, protocol)
protocol._send_message(
ThpSelectMethod(selected_pairing_method=ThpPairingMethod.QrCode)
)
protocol._read_message(ThpPairingPreparationsFinished)
# QR Code shown
protocol._read_message(ButtonRequest)
protocol._send_message(ButtonAck())
# Read code from "Trezor's display" using debuglink
pairing_info = client.debug.pairing_info(
thp_channel_id=protocol.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_qr_code
# Compute tag for response
sha_ctx = sha256(protocol.handshake_hash)
sha_ctx.update(code)
tag = sha_ctx.digest()
protocol._send_message(ThpQrCodeTag(tag=tag))
secret_msg = protocol._read_message(ThpQrCodeSecret)
# Check that the `code` was derived from the revealed secret
sha_ctx = sha256(ThpPairingMethod.QrCode.to_bytes(1, "big"))
sha_ctx.update(protocol.handshake_hash)
sha_ctx.update(secret_msg.secret)
computed_code = sha_ctx.digest()[:16]
assert code == computed_code
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)
protocol._has_valid_channel = True
def test_pairing_code_entry(client: Client) -> None:
protocol = _prepare_protocol_for_pairing(client)
_handle_pairing_request(client, protocol)
protocol._send_message(
ThpSelectMethod(selected_pairing_method=ThpPairingMethod.CodeEntry)
)
commitment_msg = protocol._read_message(ThpCodeEntryCommitment)
commitment = commitment_msg.commitment
challenge = random.randbytes(16)
protocol._send_message(ThpCodeEntryChallenge(challenge=challenge))
cpace_trezor = protocol._read_message(ThpCodeEntryCpaceTrezor)
cpace_trezor_public_key = cpace_trezor.cpace_trezor_public_key
# Code Entry code shown
protocol._read_message(ButtonRequest)
protocol._send_message(ButtonAck())
pairing_info = client.debug.pairing_info(
thp_channel_id=protocol.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_entry_code
cpace = Cpace(handshake_hash=protocol.handshake_hash)
cpace.random_bytes = random.randbytes
cpace.generate_keys_and_secret(code.to_bytes(6, "big"), cpace_trezor_public_key)
sha_ctx = sha256(cpace.shared_secret)
tag = sha_ctx.digest()
protocol._send_message(
ThpCodeEntryCpaceHostTag(
cpace_host_public_key=cpace.host_public_key,
tag=tag,
)
)
secret_msg = protocol._read_message(ThpCodeEntrySecret)
# Check `commitment` and `code`
sha_ctx = sha256(secret_msg.secret)
computed_commitment = sha_ctx.digest()
assert commitment == computed_commitment
sha_ctx = sha256(ThpPairingMethod.CodeEntry.to_bytes(1, "big"))
sha_ctx.update(protocol.handshake_hash)
sha_ctx.update(secret_msg.secret)
sha_ctx.update(challenge)
code_hash = sha_ctx.digest()
computed_code = int.from_bytes(code_hash, "big") % 1000000
assert code == computed_code
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)
protocol._has_valid_channel = True
def test_pairing_nfc(client: Client) -> None:
protocol = _prepare_protocol_for_pairing(client)
_nfc_pairing(client, protocol)
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)
protocol._has_valid_channel = True
def _nfc_pairing(client: Client, protocol: ProtocolV2):
_handle_pairing_request(client, protocol)
protocol._send_message(
ThpSelectMethod(selected_pairing_method=ThpPairingMethod.NFC)
)
protocol._read_message(ThpPairingPreparationsFinished)
# NFC screen shown
protocol._read_message(ButtonRequest)
protocol._send_message(ButtonAck())
nfc_secret_host = random.randbytes(16)
# Read `nfc_secret` and `handshake_hash` from Trezor using debuglink
pairing_info = client.debug.pairing_info(
thp_channel_id=protocol.channel_id.to_bytes(2, "big"),
handshake_hash=protocol.handshake_hash,
nfc_secret_host=nfc_secret_host,
)
handshake_hash_trezor = pairing_info.handshake_hash
nfc_secret_trezor = pairing_info.nfc_secret_trezor
assert handshake_hash_trezor[:16] == protocol.handshake_hash[:16]
# Compute tag for response
sha_ctx = sha256(ThpPairingMethod.NFC.to_bytes(1, "big"))
sha_ctx.update(protocol.handshake_hash)
sha_ctx.update(nfc_secret_trezor)
tag_host = sha_ctx.digest()
protocol._send_message(ThpNfcTagHost(tag=tag_host))
tag_trezor_msg = protocol._read_message(ThpNfcTagTrezor)
# Check that the `code` was derived from the revealed secret
sha_ctx = sha256(ThpPairingMethod.NFC.to_bytes(1, "big"))
sha_ctx.update(protocol.handshake_hash)
sha_ctx.update(nfc_secret_host)
computed_tag = sha_ctx.digest()
assert tag_trezor_msg.tag == computed_tag
def test_credential_phase(client: Client):
protocol = _prepare_protocol_for_pairing(client)
_nfc_pairing(client, protocol)
# Request credential with confirmation after pairing
host_static_privkey = curve25519.get_private_key(os.urandom(32))
host_static_pubkey = curve25519.get_public_key(host_static_privkey)
protocol._send_message(
ThpCredentialRequest(host_static_pubkey=host_static_pubkey, autoconnect=False)
)
credential_response = protocol._read_message(ThpCredentialResponse)
assert credential_response.credential is not None
credential = credential_response.credential
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)
# Connect using credential with confirmation
protocol = _prepare_protocol(client)
protocol._do_channel_allocation()
protocol._do_handshake(credential, host_static_privkey)
protocol._send_message(ThpEndRequest())
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "connection_request"
protocol._send_message(ButtonAck())
client.debug.press_yes()
protocol._read_message(ThpEndResponse)
# Connect using credential with confirmation and ask for autoconnect credential
protocol = _prepare_protocol(client)
protocol._do_channel_allocation()
protocol._do_handshake(credential, host_static_privkey)
protocol._send_message(
ThpCredentialRequest(host_static_pubkey=host_static_pubkey, autoconnect=True)
)
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "connection_request"
protocol._send_message(ButtonAck())
client.debug.press_yes()
credential_response_2 = protocol._read_message(ThpCredentialResponse)
assert credential_response_2.credential is not None
credential_auto = credential_response_2.credential
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)
# Connect using autoconnect credential
protocol = _prepare_protocol(client)
protocol._do_channel_allocation()
protocol._do_handshake(credential_auto, host_static_privkey)
protocol._send_message(ThpEndRequest())
protocol._read_message(ThpEndResponse)