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

View File

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

View File

@ -85,7 +85,7 @@ message DebugLinkRecordScreen {
}
/**
* Request: Computer asks for device state
* Request: Host asks for device state
* @start
* @next DebugLinkState
*/
@ -134,6 +134,29 @@ message DebugLinkState {
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
* @start

View File

@ -9,6 +9,224 @@ import "options.proto";
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.
* @embed
@ -16,6 +234,7 @@ option (include_in_bitcoin_only) = true;
message ThpCredentialMetadata {
option (internal_only) = true;
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_DebugLinkResetDebugEvents = 9007 [(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
MessageType_EthereumGetPublicKey = 450 [(wire_in) = true];

View File

@ -62,6 +62,7 @@ INT_TYPES = (
)
MESSAGE_TYPE_ENUM = "MessageType"
THP_MESSAGE_TYPE_ENUM = "ThpMessageType"
LengthDelimited = c.Struct(
"len" / c.VarInt,
@ -239,6 +240,9 @@ class ProtoMessage:
@classmethod
def from_message(cls, descriptor: "Descriptor", message):
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)
extensions = descriptor.get_extensions(message_type)
# override with extensions set on the message itself
@ -248,6 +252,8 @@ class ProtoMessage:
wire_type = extensions["wire_type"]
elif message_type is not None:
wire_type = message_type.number
elif thp_message_type is not None:
wire_type = thp_message_type.number
else:
wire_type = None
@ -351,10 +357,13 @@ class Descriptor:
]
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)
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.thp_message_type_enum)
# find messages and enums
self.messages = []
@ -423,6 +432,8 @@ class Descriptor:
self._nested_types_from_message(nested.orig)
def convert_enum_value_names(self, enum):
if isinstance(enum,tuple):
return
for value in enum.value:
value.name = strip_enum_prefix(enum.name, value.name)
@ -558,6 +569,8 @@ class RustBlobRenderer:
enums = []
cursor = 0
for enum in sorted(self.descriptor.enums, key=lambda e: e.name):
if enum.name == "MessageType":
continue
self.enum_map[enum.name] = cursor
enum_blob = ENUM_ENTRY.build(sorted(v.number for v in enum.value))
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
$(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
$(SCONS) --max-drift=1 $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \
TREZOR_EMULATOR_ASAN=1 TREZOR_EMULATOR_DEBUGGABLE=1

View File

@ -578,14 +578,23 @@ if FROZEN:
] 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/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',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
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/enums/*.py',

View File

@ -637,14 +637,23 @@ if FROZEN:
] 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/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',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if 'sd_card' not in FEATURES_AVAILABLE else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
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/enums/*.py',

View File

@ -51,6 +51,8 @@ storage.cache_codec
import storage.cache_codec
storage.cache_common
import storage.cache_common
storage.cache_thp
import storage.cache_thp
storage.common
import storage.common
storage.debug
@ -419,10 +421,50 @@ apps.workflow_handlers
import apps.workflow_handlers
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
import apps.thp
apps.thp.credential_manager
import apps.thp.credential_manager
apps.thp.pairing
import apps.thp.pairing
if not utils.BITCOIN_ONLY:
trezor.enums.BinanceOrderSide

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import storage.device as storage_device
from storage.cache_common import APP_COMMON_BUSY_DEADLINE_MS, APP_COMMON_SEED
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.ui.layouts import confirm_action
from trezor.wire import context
@ -27,6 +27,9 @@ if TYPE_CHECKING:
)
from trezor.wire import Handler, Msg
if utils.USE_THP:
from trezor.messages import Failure, ThpCreateNewSession
_SCREENSAVER_IS_ON = False
@ -204,7 +207,75 @@ def get_features() -> Features:
return f
async def handle_Initialize(msg: Initialize) -> Features:
if utils.USE_THP:
async def handle_ThpCreateNewSession(
message: ThpCreateNewSession,
) -> Success | Failure:
"""
Creates a new `ThpSession` based on the provided parameters and returns a
`Success` message on success.
Returns an appropriate `Failure` message if session creation fails.
"""
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
from apps.common.seed import derive_and_store_roots
ctx = get_context()
# Assert that context `ctx` is `GenericSessionContext`
assert isinstance(ctx, GenericSessionContext)
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)
@ -226,7 +297,9 @@ async def handle_Initialize(msg: Initialize) -> Features:
have_seed = False
if not have_seed:
context.cache_set_bool(APP_COMMON_DERIVE_CARDANO, bool(msg.derive_cardano))
context.cache_set_bool(
APP_COMMON_DERIVE_CARDANO, bool(msg.derive_cardano)
)
features = get_features()
features.session_id = session_id
@ -464,8 +537,12 @@ def boot() -> None:
MT = MessageType # local_cache_global
# 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 [
(MT.Initialize, handle_Initialize),
(MT.GetFeatures, handle_GetFeatures),
(MT.Cancel, handle_Cancel),
(MT.LockDevice, handle_LockDevice),

View File

@ -6,9 +6,8 @@ from storage.cache_common import (
APP_CARDANO_ICARUS_TREZOR_SECRET,
APP_COMMON_DERIVE_CARDANO,
)
from trezor import wire
from trezor import utils, wire
from trezor.crypto import cardano
from trezor.wire import context
from apps.common import mnemonic
from apps.common.seed import get_seed
@ -21,6 +20,7 @@ if TYPE_CHECKING:
from trezor import messages
from trezor.crypto import bip32
from trezor.enums import CardanoDerivationType
from trezor.wire.protocol_common import Context
from apps.common.keychain import Handler, MsgOut
from apps.common.paths import Bip32Path
@ -116,9 +116,9 @@ def is_minting_path(path: Bip32Path) -> bool:
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 context.cache_get_bool(APP_COMMON_DERIVE_CARDANO)
assert ctx.cache.get_bool(APP_COMMON_DERIVE_CARDANO)
if not mnemonic.is_bip39():
# 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:
icarus_trezor_secret = icarus_secret
context.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_SECRET, icarus_secret)
ctx.cache.set(APP_CARDANO_ICARUS_TREZOR_SECRET, icarus_trezor_secret)
async def _get_keychain_bip39(derivation_type: CardanoDerivationType) -> Keychain:
from trezor.enums import CardanoDerivationType
from apps.common.seed import derive_and_store_roots
from trezor.wire import context
if not device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
@ -164,8 +163,11 @@ async def _get_keychain_bip39(derivation_type: CardanoDerivationType) -> Keychai
# _get_secret
secret = context.cache_get(cache_entry)
if not utils.USE_THP:
if secret is None:
await derive_and_store_roots()
from apps.common.seed import derive_and_store_roots_legacy
await derive_and_store_roots_legacy()
secret = context.cache_get(cache_entry)
assert secret is not None

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
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.wire import context
from trezor.wire.message_handler import filters, remove_filter
@ -24,14 +24,23 @@ def deactivate_repeated_backup() -> None:
remove_filter(_repeated_backup_filter)
_ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = (
if utils.USE_THP:
_ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = (
MessageType.GetFeatures,
MessageType.EndSession,
MessageType.BackupDevice,
MessageType.WipeDevice,
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]:

View File

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

View File

@ -1,16 +1,78 @@
from micropython import const
from typing import TYPE_CHECKING
import storage.device as storage_device
from trezor import utils
from trezor.wire import DataError
_MAX_PASSPHRASE_LEN = const(50)
if TYPE_CHECKING:
from trezor.messages import ThpCreateNewSession
def is_enabled() -> bool:
return storage_device.is_passphrase_enabled()
async def get() -> str:
async def get_passphrase(msg: ThpCreateNewSession) -> str:
if not is_enabled():
return ""
if msg.on_device or storage_device.get_passphrase_always_on_device():
passphrase = await _get_on_device()
else:
passphrase = msg.passphrase or ""
if passphrase:
await _handle_displaying_passphrase_from_host(passphrase)
if len(passphrase.encode()) > _MAX_PASSPHRASE_LEN:
raise DataError(f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes")
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():
@ -24,13 +86,13 @@ async def get() -> str:
else:
passphrase = await _request_on_host()
if len(passphrase.encode()) > _MAX_PASSPHRASE_LEN:
raise DataError(f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes")
raise DataError(
f"Maximum passphrase length is {_MAX_PASSPHRASE_LEN} bytes"
)
return passphrase
async def _request_on_host() -> str:
from trezor import TR
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
@ -55,30 +117,6 @@ async def _request_on_host() -> str:
# 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,
)
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.crypto import hmac
from trezor.wire import context
from trezor.wire.context import get_context
from trezor.wire.errors import DataError
from apps.common import cache
from . import mnemonic
from .passphrase import get as get_passphrase
from .passphrase import get_passphrase as get_passphrase
if TYPE_CHECKING:
from trezor.crypto import bip32
from trezor.messages import ThpCreateNewSession
from trezor.wire.protocol_common import Context
from .paths import Bip32Path, Slip21Path
@ -22,6 +26,9 @@ if not utils.BITCOIN_ONLY:
APP_COMMON_DERIVE_CARDANO,
)
if not utils.USE_THP:
from .passphrase import get as get_passphrase_legacy
class Slip21Node:
"""
@ -54,51 +61,111 @@ class Slip21Node:
return Slip21Node(data=self.data)
if not utils.BITCOIN_ONLY:
# === 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
if utils.USE_THP:
async def get_seed() -> bytes: # type: ignore [Function declaration "get_seed" is obscured by a declaration of the same name]
common_seed = context.cache_get(APP_COMMON_SEED)
assert common_seed is not None
return common_seed
if utils.BITCOIN_ONLY:
# === Bitcoin_only variant ===
# We want to derive the normal seed ONLY
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!")
if ctx.cache.is_set(APP_COMMON_SEED):
raise Exception("Seed is already set!")
async def derive_and_store_roots() -> None:
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:
passphrase = await get_passphrase(msg)
common_seed = mnemonic.get_seed(passphrase)
context.cache_set(APP_COMMON_SEED, common_seed)
ctx.cache.set(APP_COMMON_SEED, common_seed)
if need_cardano_secret:
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
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)
assert common_seed is not None
return common_seed
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()
return mnemonic.get_seed(passphrase)
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)

View File

@ -1,3 +1,5 @@
from trezor.wire import message_handler
if not __debug__:
from trezor.utils import halt
@ -22,20 +24,23 @@ if __debug__:
from trezor.messages import (
DebugLinkDecision,
DebugLinkEraseSdCard,
DebugLinkGetPairingInfo,
DebugLinkGetState,
DebugLinkOptigaSetSecMax,
DebugLinkPairingInfo,
DebugLinkRecordScreen,
DebugLinkReseedRandom,
DebugLinkState,
)
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]]
layout_change_box = loop.mailbox()
DEBUG_CONTEXT: context.Context | None = None
DEBUG_CONTEXT: Context | None = None
REFRESH_INDEX = 0
@ -70,9 +75,7 @@ if __debug__:
"layout deadlock detected (did you send a ButtonAck?)"
)
async def return_layout_change(
ctx: wire.protocol_common.Context, detect_deadlock: bool = False
) -> None:
async def return_layout_change(ctx: Context, detect_deadlock: bool = False) -> None:
# set up the wait
storage.layout_watcher = True
@ -265,6 +268,42 @@ if __debug__:
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(
msg: DebugLinkGetState,
) -> DebugLinkState | None:
@ -395,7 +434,6 @@ if __debug__:
ctx.iface.iface_num(),
msg_type,
)
if msg.type not in WORKFLOW_HANDLERS:
await ctx.write(wire.message_handler.unexpected_message())
continue
@ -408,7 +446,7 @@ if __debug__:
await ctx.write(Success())
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:
res_msg = await WORKFLOW_HANDLERS[msg.type](req_msg)
except Exception as exc:
@ -427,6 +465,7 @@ if __debug__:
WORKFLOW_HANDLERS: dict[int, Handler] = {
MessageType.DebugLinkDecision: dispatch_DebugLinkDecision,
MessageType.DebugLinkGetState: dispatch_DebugLinkGetState,
MessageType.DebugLinkGetPairingInfo: dispatch_DebugLinkGetPairingInfo,
MessageType.DebugLinkReseedRandom: dispatch_DebugLinkReseedRandom,
MessageType.DebugLinkRecordScreen: dispatch_DebugLinkRecordScreen,
MessageType.DebugLinkEraseSdCard: dispatch_DebugLinkEraseSdCard,

View File

@ -89,7 +89,7 @@ async def reboot_to_bootloader(msg: RebootToBootloader) -> NoReturn:
boot_args = None
ctx = get_context()
await ctx.write(Success(message="Rebooting"))
await ctx.write_force(Success(message="Rebooting"))
# make sure the outgoing USB buffer is flushed
await loop.wait(ctx.iface.iface_num() | io.POLL_WRITE)
# 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.enums import BackupType, ButtonRequestType
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.request_pin import (
@ -69,8 +70,8 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
if recovery_type == RecoveryType.NormalRecovery:
await confirm_reset_device(recovery=True)
# wipe storage to make sure the device is in a clear state
storage.reset()
# wipe storage to make sure the device is in a clear state (except protocol cache)
storage.reset(excluded=try_get_ctx_ids())
# set up pin if requested
if msg.pin_protection:

View File

@ -3,8 +3,9 @@ from typing import TYPE_CHECKING
import storage.device as storage_device
import storage.recovery as storage_recovery
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.wire import message_handler
from apps.common import backup_types
@ -38,7 +39,13 @@ async def recovery_process() -> Success:
recovery_type = storage_recovery.get_type()
wire.message_handler.AVOID_RESTARTING_FOR = (
if utils.USE_THP:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.GetFeatures,
MessageType.EndSession,
)
else:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.Initialize,
MessageType.GetFeatures,
MessageType.EndSession,
@ -49,7 +56,10 @@ async def recovery_process() -> Success:
storage_recovery.end_progress()
backup.deactivate_repeated_backup()
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
@ -59,7 +69,13 @@ async def _continue_repeated_backup() -> None:
from apps.common import backup
from apps.management.backup_device import perform_backup
wire.message_handler.AVOID_RESTARTING_FOR = (
if utils.USE_THP:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.GetFeatures,
MessageType.EndSession,
)
else:
message_handler.AVOID_RESTARTING_FOR = (
MessageType.Initialize,
MessageType.GetFeatures,
MessageType.EndSession,

View File

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

View File

@ -1,12 +1,19 @@
from typing import TYPE_CHECKING
from trezor.wire.context import get_context, try_get_ctx_ids
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
from trezor import TR, config, translations
from trezor import TR, config, loop, translations
from trezor.enums import ButtonRequestType
from trezor.messages import Success
from trezor.pin import render_empty_loader
@ -26,16 +33,31 @@ async def wipe_device(msg: WipeDevice) -> Success:
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
render_empty_loader(config.StorageMessage.PROCESSING_MSG)
# wipe storage
storage.wipe()
storage.wipe(clear_cache=False)
# clear cache - exclude current context
storage.wipe_cache(excluded=try_get_ctx_ids())
# erase translations
translations.deinit()
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_from_storage()
return Success(message="Device wiped")
loop.clear()
if __debug__:
log.debug(__name__, "Device wipe - finished")

View File

@ -63,17 +63,26 @@ def issue_credential(
return credential_raw
def validate_credential(
def decode_credential(
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,
) -> bool:
"""
Validate a pairing credential binded to the provided host static public 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(
host_static_pubkey=host_static_pubkey,
cred_metadata=credential.cred_metadata,
@ -83,6 +92,27 @@ def validate_credential(
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:
msg_len = protobuf.encoded_length(msg)
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,14 +1,31 @@
# 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
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
config.wipe()
if clear_cache:
cache.clear_all()
def wipe_cache(excluded: Tuple[bytes, bytes] | None = None) -> None:
cache.clear_all(excluded)
def init_unlocked() -> None:
# Check for storage version upgrade.
version = device.get_version()
@ -21,12 +38,13 @@ def init_unlocked() -> None:
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.
"""
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)

View File

@ -1,25 +1,46 @@
import builtins
import gc
from typing import TYPE_CHECKING
from storage import cache_codec
from storage.cache_common import SESSIONLESS_FLAG, SessionlessCache
from trezor import utils
if TYPE_CHECKING:
from typing import Tuple
pass
# Cache initialization
_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()
_SESSIONLESS_CACHE.clear()
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.
"""
global autolock_last_touch
autolock_last_touch = None
_SESSIONLESS_CACHE.clear()
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()

View File

@ -14,6 +14,14 @@ if not utils.BITCOIN_ONLY:
APP_CARDANO_ICARUS_TREZOR_SECRET = const(6)
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
SESSIONLESS_FLAG = const(128)
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
WipeCodeMismatch = 13
InvalidSession = 14
ThpUnallocatedSession = 15
InvalidProtocol = 16
BufferError = 17
DeviceIsBusy = 18
FirmwareError = 99

View File

@ -97,6 +97,8 @@ DebugLinkEraseSdCard = 9005
DebugLinkWatchLayout = 9006
DebugLinkResetDebugEvents = 9007
DebugLinkOptigaSetSecMax = 9008
DebugLinkGetPairingInfo = 9009
DebugLinkPairingInfo = 9010
BenchmarkListNames = 9100
BenchmarkNames = 9101
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
WipeCodeMismatch = 13
InvalidSession = 14
ThpUnallocatedSession = 15
InvalidProtocol = 16
BufferError = 17
DeviceIsBusy = 18
FirmwareError = 99
class ButtonRequestType(IntEnum):
@ -347,6 +351,32 @@ if TYPE_CHECKING:
Nay = 1
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):
Initialize = 0
Ping = 1
@ -447,6 +477,8 @@ if TYPE_CHECKING:
DebugLinkWatchLayout = 9006
DebugLinkResetDebugEvents = 9007
DebugLinkOptigaSetSecMax = 9008
DebugLinkGetPairingInfo = 9009
DebugLinkPairingInfo = 9010
EthereumGetPublicKey = 450
EthereumPublicKey = 451
EthereumGetAddress = 56

View File

@ -68,6 +68,8 @@ if TYPE_CHECKING:
from trezor.enums import StellarSignerType # noqa: F401
from trezor.enums import TezosBallotType # 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
class BenchmarkListNames(protobuf.MessageType):
@ -2950,6 +2952,46 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkState"]:
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):
@classmethod
@ -6164,7 +6206,61 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["TezosManagerTransfer"]:
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"
def __init__(
@ -6174,6 +6270,220 @@ if TYPE_CHECKING:
) -> None:
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
def is_type_of(cls, msg: Any) -> TypeGuard["ThpCredentialMetadata"]:
return isinstance(msg, cls)

View File

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

View File

@ -5,7 +5,7 @@ Handles on-the-wire communication with a host computer. The communication is:
- Request / response.
- 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.
This module:
@ -29,7 +29,12 @@ from typing import TYPE_CHECKING
from trezor import log, loop, protobuf, utils
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 .message_handler import failure
@ -37,10 +42,6 @@ from .message_handler import failure
# other packages.
from .errors import * # isort:skip # noqa: F401,F403
_PROTOBUF_BUFFER_SIZE = const(8192)
WIRE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
if TYPE_CHECKING:
from trezorio import WireInterface
from typing import Any, Callable, Coroutine, TypeVar
@ -57,7 +58,37 @@ def setup(iface: WireInterface) -> None:
loop.schedule(handle_session(iface))
async def handle_session(iface: WireInterface) -> None:
if utils.USE_THP:
# memory_manager is imported to create READ/WRITE buffers
# in more stable area of memory
from .thp import memory_manager # noqa: F401
async def handle_session(iface: WireInterface) -> None:
# 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:
await thp_main.thp_main_loop(iface)
except Exception as exc:
# Log and try again.
if __debug__:
log.exception(__name__, exc)
finally:
# 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)
loop.clear()
return # pylint: disable=lost-exception
else:
_PROTOBUF_BUFFER_SIZE = const(8192)
WIRE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE)
async def handle_session(iface: WireInterface) -> None:
ctx = CodecContext(iface, WIRE_BUFFER)
next_msg: protocol_common.Message | None = None
@ -84,7 +115,9 @@ async def handle_session(iface: WireInterface) -> None:
do_not_restart = False
try:
do_not_restart = await message_handler.handle_single_message(ctx, msg)
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...
@ -103,6 +136,8 @@ async def handle_session(iface: WireInterface) -> None:
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

View File

@ -17,7 +17,7 @@ from typing import TYPE_CHECKING
from storage import cache
from storage.cache_common import SESSIONLESS_FLAG
from trezor import loop, protobuf
from trezor import loop, protobuf, utils
from .protocol_common import Context, Message
@ -138,6 +138,17 @@ def with_context(ctx: Context, workflow: loop.Task) -> Generator:
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
if TYPE_CHECKING:

View File

@ -8,6 +8,17 @@ class Error(Exception):
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):
def __init__(self, message: str) -> None:
super().__init__(FailureType.UnexpectedMessage, message)

View File

@ -25,7 +25,12 @@ def wrap_protobuf_load(
expected_type: type[LoadedMessageType],
) -> LoadedMessageType:
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(
__name__,
"Buffer to be parsed to a LoadedMessage: %s",
@ -38,7 +43,7 @@ def wrap_protobuf_load(
)
return msg
except Exception as e:
if __debug__:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
log.exception(__name__, e)
if 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")
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:
"""Handle a message that was loaded from a WireInterface by the caller.
@ -60,11 +84,21 @@ 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,
this function will return `True`.
"""
if __debug__:
if __debug__ and utils.ALLOW_DEBUG_MESSAGES:
try:
msg_type = protobuf.type_for_wire(msg.type).MESSAGE_NAME
except Exception:
msg_type = f"{msg.type} - unknown message type"
if utils.USE_THP:
cid = int.from_bytes(ctx.channel_id, "big")
log.debug(
__name__,
"%d:%d receive: <%s>",
ctx.iface.iface_num(),
cid,
msg_type,
)
else:
log.debug(
__name__,
"%d receive: <%s>",
@ -132,7 +166,7 @@ async def handle_single_message(ctx: Context, msg: Message) -> bool:
# - 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 __debug__ and utils.ALLOW_DEBUG_MESSAGES:
if isinstance(exc, ActionCancelled):
log.debug(__name__, "cancelled: %s", exc.message)
elif isinstance(exc, loop.TaskClosed):

View File

@ -4,7 +4,7 @@ from trezor import protobuf
if TYPE_CHECKING:
from trezorio import WireInterface
from typing import Container, TypeVar, overload
from typing import Awaitable, Container, TypeVar, overload
from storage.cache_common import DataCache
@ -72,6 +72,9 @@ class Context:
"""Write a message to the wire."""
...
def write_force(self, msg: protobuf.MessageType) -> Awaitable[None]:
return self.write(msg)
async def call(
self,
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
from trezor import log, loop
from trezor.enums import MessageType
from trezor.enums import MessageType, ThpMessageType
if TYPE_CHECKING:
from typing import Callable
@ -17,8 +17,20 @@ if __debug__:
from trezor import utils
ALLOW_WHILE_LOCKED = (
if utils.USE_THP:
ALLOW_WHILE_LOCKED = (
ThpMessageType.ThpCreateNewSession,
MessageType.EndSession,
MessageType.GetFeatures,
MessageType.Cancel,
MessageType.LockDevice,
MessageType.DoPreauthorized,
MessageType.WipeDevice,
MessageType.SetBusy,
MessageType.Ping,
)
else:
ALLOW_WHILE_LOCKED = (
MessageType.Initialize,
MessageType.EndSession,
MessageType.GetFeatures,
@ -28,7 +40,7 @@ ALLOW_WHILE_LOCKED = (
MessageType.WipeDevice,
MessageType.SetBusy,
MessageType.Ping,
)
)
# 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
from trezor import wire
@ -20,8 +21,22 @@ from apps.bitcoin.sign_tx.bitcoin import Bitcoin
from apps.bitcoin.sign_tx.tx_info import TxInfo
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):
if utils.USE_THP:
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))
@ -54,6 +69,7 @@ class TestApprover(unittest.TestCase):
coin_name=self.coin.coin_name,
script_type=InputScriptType.SPENDTAPROOT,
)
if not utils.USE_THP:
storage.cache_codec.start_session()
def make_coinjoin_request(self, inputs):

View File

@ -1,21 +1,36 @@
from common import H_, unittest # isort:skip
# flake8: noqa: F403,F405
from common import * # isort:skip
import storage.cache_codec
from trezor.enums import InputScriptType
from trezor.messages import AuthorizeCoinJoin, GetOwnershipProof, SignTx
from trezor.wire import context
from trezor.wire.codec.codec_context import CodecContext
from apps.bitcoin.authorization import CoinJoinAuthorization
from apps.common import coins
_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):
coin = coins.by_name("Bitcoin")
if utils.USE_THP:
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))
@ -34,6 +49,7 @@ class TestAuthorization(unittest.TestCase):
)
self.authorization = CoinJoinAuthorization(self.msg_auth)
if not utils.USE_THP:
storage.cache_codec.start_session()
def test_ownership_proof_account_depth_mismatch(self):

View File

@ -1,7 +1,7 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
from storage import cache_codec, cache_common
from storage import cache_common
from trezor import wire
from trezor.crypto import bip39
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
if utils.USE_THP:
import thp_common
else:
from storage import cache_codec
class TestBitcoinKeychain(unittest.TestCase):
if utils.USE_THP:
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 tearDownClass(self):
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 tearDownClass(self):
context.CURRENT_CONTEXT = None
def test_bitcoin(self):
coin = _get_coin_by_name("Bitcoin")
keychain = await_result(_get_keychain_for_coin(coin))
@ -98,18 +116,30 @@ class TestBitcoinKeychain(unittest.TestCase):
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestAltcoinKeychains(unittest.TestCase):
if utils.USE_THP:
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 tearDownClass(self):
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 tearDownClass(self):
context.CURRENT_CONTEXT = None
def test_bcash(self):
coin = _get_coin_by_name("Bcash")
keychain = await_result(_get_keychain_for_coin(coin))

View File

@ -2,7 +2,7 @@
from common import * # isort:skip
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.crypto import bip39
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.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):
if utils.USE_THP:
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):
context.CURRENT_CONTEXT = None
def setUp(self):
cache_codec.start_session()
def tearDownClass(self):
context.CURRENT_CONTEXT = None
def tearDown(self):
cache.clear_all()

View File

@ -3,7 +3,7 @@ from common import * # isort:skip
import unittest
from storage import cache_codec, cache_common
from storage import cache_common
from trezor import wire
from trezor.crypto import bip39
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.paths import HARDENED
if utils.USE_THP:
import thp_common
else:
from storage import cache_codec
if not utils.BITCOIN_ONLY:
from ethereum_common import encode_network, make_network
from trezor.messages import (
@ -74,17 +80,30 @@ class TestEthereumKeychain(unittest.TestCase):
addr,
)
if utils.USE_THP:
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 tearDownClass(self):
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 tearDownClass(self):
context.CURRENT_CONTEXT = None
def from_address_n(self, address_n):
slip44 = _slip44_from_address_n(address_n)
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)
self.assertNotEqual(cred_1, cred_3)
self.assertTrue(credential_manager.validate_credential(cred_1, DUMMY_KEY_1))
self.assertTrue(credential_manager.validate_credential(cred_3, DUMMY_KEY_1))
self.assertFalse(credential_manager.validate_credential(cred_1, DUMMY_KEY_2))
self.assertTrue(
credential_manager.decode_and_validate_credential(cred_1, DUMMY_KEY_1)
)
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()
cred_4 = _issue_credential(HOST_NAME_1, DUMMY_KEY_1)
self.assertNotEqual(cred_1, cred_4)
self.assertFalse(credential_manager.validate_credential(cred_1, DUMMY_KEY_1))
self.assertFalse(credential_manager.validate_credential(cred_3, DUMMY_KEY_1))
self.assertTrue(credential_manager.validate_credential(cred_4, DUMMY_KEY_1))
self.assertFalse(
credential_manager.decode_and_validate_credential(cred_1, 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):
"""

View File

@ -1,29 +1,361 @@
# flake8: noqa: F403,F405
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
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
def is_session_started() -> bool:
return cache_codec._active_session_idx is not None
_PROTOCOL_CACHE = cache_thp
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):
if utils.USE_THP:
def setUpClass(self):
if __debug__:
thp_common.suppres_debug_log()
super().__init__()
def setUp(self):
self.interface = MockHID(0xDEADBEEF)
cache.clear_all()
def test_new_channel_and_session(self):
channel = thp_common.get_new_channel(self.interface)
# Assert that channel is created without any sessions
self.assertEqual(len(channel.sessions), 0)
cid_1 = channel.channel_id
session_cache_1 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x01"
)
session_1 = SessionContext(channel, session_cache_1)
self.assertEqual(session_1.channel_id, cid_1)
session_cache_2 = cache_thp.create_or_replace_session(
channel.channel_cache, b"\x02"
)
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)
channel_2 = thp_common.get_new_channel(self.interface)
cid_2 = channel_2.channel_id
self.assertNotEqual(cid_1, cid_2)
session_cache_3 = cache_thp.create_or_replace_session(
channel_2.channel_cache, b"\x01"
)
session_3 = SessionContext(channel_2, session_cache_3)
self.assertEqual(session_3.channel_id, cid_2)
# Sessions 1 and 3 should have different channel_id, but the same session_id
self.assertNotEqual(session_1.channel_id, session_3.channel_id)
self.assertEqual(session_1.session_id, session_3.session_id)
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):
@ -36,18 +368,18 @@ class TestStorageCache(unittest.TestCase):
self.assertNotEqual(session_id_a, session_id_b)
cache.clear_all()
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_set(KEY, "something")
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_get(KEY)
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())
context.cache_set(KEY, b"A")
get_active_session().set(KEY, b"A")
cache_codec.end_current_session()
self.assertFalse(is_session_started())
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
self.assertIsNone(get_active_session())
# ending an ended session should be a no-op
cache_codec.end_current_session()
@ -57,7 +389,7 @@ class TestStorageCache(unittest.TestCase):
# original session no longer exists
self.assertNotEqual(session_id_a, session_id)
# original session data no longer exists
self.assertIsNone(context.cache_get(KEY))
self.assertIsNone(get_active_session().get(KEY))
# create a new session
session_id_b = cache_codec.start_session()
@ -73,70 +405,69 @@ class TestStorageCache(unittest.TestCase):
def test_session_queue(self):
session_id = cache_codec.start_session()
self.assertEqual(cache_codec.start_session(session_id), session_id)
context.cache_set(KEY, b"A")
for _ in range(cache_codec._MAX_SESSIONS_COUNT):
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(context.cache_get(KEY))
self.assertIsNone(get_active_session().get(KEY))
def test_get_set(self):
session_id1 = cache_codec.start_session()
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
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()
context.cache_set(KEY, b"world")
self.assertEqual(context.cache_get(KEY), b"world")
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(context.cache_get(KEY), b"world")
self.assertEqual(cache_codec.get_active_session().get(KEY), b"world")
cache_codec.start_session(session_id1)
self.assertEqual(context.cache_get(KEY), b"hello")
self.assertEqual(cache_codec.get_active_session().get(KEY), b"hello")
cache.clear_all()
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_get(KEY)
self.assertIsNone(cache_codec.get_active_session())
def test_get_set_int(self):
session_id1 = cache_codec.start_session()
context.cache_set_int(KEY, 1234)
self.assertEqual(context.cache_get_int(KEY), 1234)
get_active_session().set_int(KEY, 1234)
self.assertEqual(get_active_session().get_int(KEY), 1234)
session_id2 = cache_codec.start_session()
context.cache_set_int(KEY, 5678)
self.assertEqual(context.cache_get_int(KEY), 5678)
get_active_session().set_int(KEY, 5678)
self.assertEqual(get_active_session().get_int(KEY), 5678)
cache_codec.start_session(session_id2)
self.assertEqual(context.cache_get_int(KEY), 5678)
self.assertEqual(get_active_session().get_int(KEY), 5678)
cache_codec.start_session(session_id1)
self.assertEqual(context.cache_get_int(KEY), 1234)
self.assertEqual(get_active_session().get_int(KEY), 1234)
cache.clear_all()
with self.assertRaises(cache_common.InvalidSessionError):
context.cache_get_int(KEY)
self.assertIsNone(get_active_session())
def test_delete(self):
session_id1 = cache_codec.start_session()
self.assertIsNone(context.cache_get(KEY))
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
context.cache_delete(KEY)
self.assertIsNone(context.cache_get(KEY))
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))
context.cache_set(KEY, b"hello")
get_active_session().set(KEY, b"hello")
cache_codec.start_session()
self.assertIsNone(context.cache_get(KEY))
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
context.cache_delete(KEY)
self.assertIsNone(context.cache_get(KEY))
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))
cache_codec.start_session(session_id1)
self.assertEqual(context.cache_get(KEY), b"hello")
self.assertEqual(get_active_session().get(KEY), b"hello")
def test_decorators(self):
run_count = 0
cache_codec.start_session()
from apps.common.cache import stored
@stored(KEY)
def func():
@ -145,43 +476,28 @@ class TestStorageCache(unittest.TestCase):
return b"foo"
# cache is empty
self.assertIsNone(context.cache_get(KEY))
self.assertIsNone(get_active_session().get(KEY))
self.assertEqual(run_count, 0)
self.assertEqual(func(), b"foo")
# function was run
self.assertEqual(run_count, 1)
self.assertEqual(context.cache_get(KEY), b"foo")
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)
@stored_async(KEY)
async def async_func():
nonlocal run_count
run_count += 1
return b"bar"
# cache is still full
self.assertEqual(await_result(async_func()), b"foo")
self.assertEqual(run_count, 1)
cache_codec.start_session()
self.assertEqual(await_result(async_func()), b"bar")
self.assertEqual(run_count, 2)
# awaitable is also run only once
self.assertEqual(await_result(async_func()), b"bar")
self.assertEqual(run_count, 2)
def test_empty_value(self):
cache_codec.start_session()
self.assertIsNone(context.cache_get(KEY))
context.cache_set(KEY, b"")
self.assertEqual(context.cache_get(KEY), b"")
self.assertIsNone(get_active_session().get(KEY))
get_active_session().set(KEY, b"")
self.assertEqual(get_active_session().get(KEY), b"")
context.cache_delete(KEY)
get_active_session().delete(KEY)
run_count = 0
from apps.common.cache import stored
@stored(KEY)
def func():
nonlocal run_count
@ -197,6 +513,8 @@ class TestStorageCache(unittest.TestCase):
@mock_storage
def test_Initialize(self):
from apps.base import handle_Initialize
def call_Initialize(**kwargs):
msg = Initialize(**kwargs)
return await_result(handle_Initialize(msg))
@ -211,31 +529,32 @@ class TestStorageCache(unittest.TestCase):
self.assertEqual(session_id, features.session_id)
# store "hello"
context.cache_set(KEY, b"hello")
get_active_session().set(KEY, b"hello")
# check that it is cleared
features = call_Initialize()
session_id = features.session_id
self.assertIsNone(context.cache_get(KEY))
self.assertIsNone(get_active_session().get(KEY))
# store "hello" again
context.cache_set(KEY, b"hello")
self.assertEqual(context.cache_get(KEY), b"hello")
get_active_session().set(KEY, b"hello")
self.assertEqual(get_active_session().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))
# supplying a different session ID starts a new session
call_Initialize(session_id=b"A" * _PROTOCOL_CACHE.SESSION_ID_LENGTH)
self.assertIsNone(get_active_session().get(KEY))
# but resuming a session loads the previous one
call_Initialize(session_id=session_id)
self.assertEqual(context.cache_get(KEY), b"hello")
self.assertEqual(get_active_session().get(KEY), b"hello")
def test_EndSession(self):
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
self.assertIsNone(get_active_session())
cache_codec.start_session()
self.assertTrue(is_session_started())
self.assertIsNone(context.cache_get(KEY))
self.assertIsNone(get_active_session().get(KEY))
await_result(handle_EndSession(EndSession()))
self.assertFalse(is_session_started())
self.assertRaises(cache_common.InvalidSessionError, context.cache_get, KEY)
self.assertIsNone(cache_codec.get_active_session())
if __name__ == "__main__":

View File

@ -3,61 +3,11 @@ from common import * # isort:skip
import ustruct
from mock_wire_interface import MockHID
from trezor import io
from trezor.loop import wait
from trezor.utils import chunks
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
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:
text = _("Invalid session");
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:
text = _("Firmware error");
break;

View File

@ -3,14 +3,14 @@ Q := @
endif
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn \
DebugLinkRecordScreen DebugLinkEraseSdCard DebugLinkWatchLayout \
DebugLinkLayout DebugLinkResetDebugEvents GetNonce \
DebugLinkRecordScreen DebugLinkEraseSdCard DebugLinkWatchLayout DebugLinkLayout \
DebugLinkResetDebugEvents DebugLinkGetPairingInfo DebugLinkPairingInfo GetNonce \
TxAckInput TxAckOutput TxAckPrev TxAckPaymentRequest \
EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \
EthereumTypedDataValueRequest EthereumTypedDataValueAck ShowDeviceTutorial \
UnlockBootloader AuthenticateDevice AuthenticityProof \
Solana StellarClaimClaimableBalanceOp \
ChangeLanguage TranslationDataRequest TranslationDataAck \
ChangeLanguage TranslationDataRequest TranslationDataAck Thp\
SetBrightness DebugLinkOptigaSetSecMax EntropyCheckReady EntropyCheckContinue \
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
typing_extensions>=4.7.1
construct-classes>=0.1.2
cryptography >=43.0.3
platformdirs >=2

View File

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

View File

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

View File

@ -414,6 +414,14 @@ pub mod failure {
Failure_WipeCodeMismatch = 13,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.common.Failure.FailureType.Failure_InvalidSession)
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)
Failure_FirmwareError = 99,
}
@ -441,6 +449,10 @@ pub mod failure {
12 => ::std::option::Option::Some(FailureType::Failure_PinMismatch),
13 => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch),
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),
_ => ::std::option::Option::None
}
@ -462,6 +474,10 @@ pub mod failure {
"Failure_PinMismatch" => ::std::option::Option::Some(FailureType::Failure_PinMismatch),
"Failure_WipeCodeMismatch" => ::std::option::Option::Some(FailureType::Failure_WipeCodeMismatch),
"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),
_ => ::std::option::Option::None
}
@ -482,6 +498,10 @@ pub mod failure {
FailureType::Failure_PinMismatch,
FailureType::Failure_WipeCodeMismatch,
FailureType::Failure_InvalidSession,
FailureType::Failure_ThpUnallocatedSession,
FailureType::Failure_InvalidProtocol,
FailureType::Failure_BufferError,
FailureType::Failure_DeviceIsBusy,
FailureType::Failure_FirmwareError,
];
}
@ -508,7 +528,11 @@ pub mod failure {
FailureType::Failure_PinMismatch => 11,
FailureType::Failure_WipeCodeMismatch => 12,
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)
}
@ -2481,9 +2505,9 @@ impl ::protobuf::reflect::ProtobufValue for HDNodeType {
static file_descriptor_proto_data: &'static [u8] = b"\
\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\
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\
\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\
\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\
@ -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\
ilure_NotInitialized\x10\x0b\x12\x17\n\x13Failure_PinMismatch\x10\x0c\
\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\
ttonRequest\x12N\n\x04code\x18\x01\x20\x01(\x0e2:.hw.trezor.messages.com\
mon.ButtonRequest.ButtonRequestTypeR\x04code\x12\x14\n\x05pages\x18\x02\
\x20\x01(\rR\x05pages\x12\x12\n\x04name\x18\x04\x20\x01(\tR\x04name\"\
\x99\x05\n\x11ButtonRequestType\x12\x17\n\x13ButtonRequest_Other\x10\x01\
\x12\"\n\x1eButtonRequest_FeeOverThreshold\x10\x02\x12\x1f\n\x1bButtonRe\
quest_ConfirmOutput\x10\x03\x12\x1d\n\x19ButtonRequest_ResetDevice\x10\
\x04\x12\x1d\n\x19ButtonRequest_ConfirmWord\x10\x05\x12\x1c\n\x18ButtonR\
equest_WipeDevice\x10\x06\x12\x1d\n\x19ButtonRequest_ProtectCall\x10\x07\
\x12\x18\n\x14ButtonRequest_SignTx\x10\x08\x12\x1f\n\x1bButtonRequest_Fi\
rmwareCheck\x10\t\x12\x19\n\x15ButtonRequest_Address\x10\n\x12\x1b\n\x17\
ButtonRequest_PublicKey\x10\x0b\x12#\n\x1fButtonRequest_MnemonicWordCoun\
t\x10\x0c\x12\x1f\n\x1bButtonRequest_MnemonicInput\x10\r\x120\n(_Depreca\
ted_ButtonRequest_PassphraseType\x10\x0e\x1a\x02\x08\x01\x12'\n#ButtonRe\
quest_UnknownDerivationPath\x10\x0f\x12\"\n\x1eButtonRequest_RecoveryHom\
epage\x10\x10\x12\x19\n\x15ButtonRequest_Success\x10\x11\x12\x19\n\x15Bu\
ttonRequest_Warning\x10\x12\x12!\n\x1dButtonRequest_PassphraseEntry\x10\
\x13\x12\x1a\n\x16ButtonRequest_PinEntry\x10\x14J\x04\x08\x03\x10\x04\"\
\x0b\n\tButtonAck\"\xbb\x02\n\x10PinMatrixRequest\x12T\n\x04type\x18\x01\
\x20\x01(\x0e2@.hw.trezor.messages.common.PinMatrixRequest.PinMatrixRequ\
estTypeR\x04type\"\xd0\x01\n\x14PinMatrixRequestType\x12\x20\n\x1cPinMat\
rixRequestType_Current\x10\x01\x12!\n\x1dPinMatrixRequestType_NewFirst\
\x10\x02\x12\"\n\x1ePinMatrixRequestType_NewSecond\x10\x03\x12&\n\"PinMa\
trixRequestType_WipeCodeFirst\x10\x04\x12'\n#PinMatrixRequestType_WipeCo\
deSecond\x10\x05\"\x20\n\x0cPinMatrixAck\x12\x10\n\x03pin\x18\x01\x20\
\x02(\tR\x03pin\"5\n\x11PassphraseRequest\x12\x20\n\n_on_device\x18\x01\
\x20\x01(\x08R\x08OnDeviceB\x02\x18\x01\"g\n\rPassphraseAck\x12\x1e\n\np\
assphrase\x18\x01\x20\x01(\tR\npassphrase\x12\x19\n\x06_state\x18\x02\
\x20\x01(\x0cR\x05StateB\x02\x18\x01\x12\x1b\n\ton_device\x18\x03\x20\
\x01(\x08R\x08onDevice\"=\n!Deprecated_PassphraseStateRequest\x12\x14\n\
\x05state\x18\x01\x20\x01(\x0cR\x05state:\x02\x18\x01\"#\n\x1dDeprecated\
_PassphraseStateAck:\x02\x18\x01\"\xc0\x01\n\nHDNodeType\x12\x14\n\x05de\
pth\x18\x01\x20\x02(\rR\x05depth\x12\x20\n\x0bfingerprint\x18\x02\x20\
\x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\x18\x03\x20\x02(\rR\x08chil\
dNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\x0cR\tchainCode\x12\x1f\n\
\x0bprivate_key\x18\x05\x20\x01(\x0cR\nprivateKey\x12\x1d\n\npublic_key\
\x18\x06\x20\x02(\x0cR\tpublicKeyB>\n#com.satoshilabs.trezor.lib.protobu\
fB\x13TrezorMessageCommon\x80\xa6\x1d\x01\
dSession\x10\x0e\x12!\n\x1dFailure_ThpUnallocatedSession\x10\x0f\x12\x1b\
\n\x17Failure_InvalidProtocol\x10\x10\x12\x17\n\x13Failure_BufferError\
\x10\x11\x12\x18\n\x14Failure_DeviceIsBusy\x10\x12\x12\x19\n\x15Failure_\
FirmwareError\x10c\"\xab\x06\n\rButtonRequest\x12N\n\x04code\x18\x01\x20\
\x01(\x0e2:.hw.trezor.messages.common.ButtonRequest.ButtonRequestTypeR\
\x04code\x12\x14\n\x05pages\x18\x02\x20\x01(\rR\x05pages\x12\x12\n\x04na\
me\x18\x04\x20\x01(\tR\x04name\"\x99\x05\n\x11ButtonRequestType\x12\x17\
\n\x13ButtonRequest_Other\x10\x01\x12\"\n\x1eButtonRequest_FeeOverThresh\
old\x10\x02\x12\x1f\n\x1bButtonRequest_ConfirmOutput\x10\x03\x12\x1d\n\
\x19ButtonRequest_ResetDevice\x10\x04\x12\x1d\n\x19ButtonRequest_Confirm\
Word\x10\x05\x12\x1c\n\x18ButtonRequest_WipeDevice\x10\x06\x12\x1d\n\x19\
ButtonRequest_ProtectCall\x10\x07\x12\x18\n\x14ButtonRequest_SignTx\x10\
\x08\x12\x1f\n\x1bButtonRequest_FirmwareCheck\x10\t\x12\x19\n\x15ButtonR\
equest_Address\x10\n\x12\x1b\n\x17ButtonRequest_PublicKey\x10\x0b\x12#\n\
\x1fButtonRequest_MnemonicWordCount\x10\x0c\x12\x1f\n\x1bButtonRequest_M\
nemonicInput\x10\r\x120\n(_Deprecated_ButtonRequest_PassphraseType\x10\
\x0e\x1a\x02\x08\x01\x12'\n#ButtonRequest_UnknownDerivationPath\x10\x0f\
\x12\"\n\x1eButtonRequest_RecoveryHomepage\x10\x10\x12\x19\n\x15ButtonRe\
quest_Success\x10\x11\x12\x19\n\x15ButtonRequest_Warning\x10\x12\x12!\n\
\x1dButtonRequest_PassphraseEntry\x10\x13\x12\x1a\n\x16ButtonRequest_Pin\
Entry\x10\x14J\x04\x08\x03\x10\x04\"\x0b\n\tButtonAck\"\xbb\x02\n\x10Pin\
MatrixRequest\x12T\n\x04type\x18\x01\x20\x01(\x0e2@.hw.trezor.messages.c\
ommon.PinMatrixRequest.PinMatrixRequestTypeR\x04type\"\xd0\x01\n\x14PinM\
atrixRequestType\x12\x20\n\x1cPinMatrixRequestType_Current\x10\x01\x12!\
\n\x1dPinMatrixRequestType_NewFirst\x10\x02\x12\"\n\x1ePinMatrixRequestT\
ype_NewSecond\x10\x03\x12&\n\"PinMatrixRequestType_WipeCodeFirst\x10\x04\
\x12'\n#PinMatrixRequestType_WipeCodeSecond\x10\x05\"\x20\n\x0cPinMatrix\
Ack\x12\x10\n\x03pin\x18\x01\x20\x02(\tR\x03pin\"5\n\x11PassphraseReques\
t\x12\x20\n\n_on_device\x18\x01\x20\x01(\x08R\x08OnDeviceB\x02\x18\x01\"\
g\n\rPassphraseAck\x12\x1e\n\npassphrase\x18\x01\x20\x01(\tR\npassphrase\
\x12\x19\n\x06_state\x18\x02\x20\x01(\x0cR\x05StateB\x02\x18\x01\x12\x1b\
\n\ton_device\x18\x03\x20\x01(\x08R\x08onDevice\"=\n!Deprecated_Passphra\
seStateRequest\x12\x14\n\x05state\x18\x01\x20\x01(\x0cR\x05state:\x02\
\x18\x01\"#\n\x1dDeprecated_PassphraseStateAck:\x02\x18\x01\"\xc0\x01\n\
\nHDNodeType\x12\x14\n\x05depth\x18\x01\x20\x02(\rR\x05depth\x12\x20\n\
\x0bfingerprint\x18\x02\x20\x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\
\x18\x03\x20\x02(\rR\x08childNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\
\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

View File

@ -2118,6 +2118,629 @@ impl ::protobuf::reflect::ProtobufValue for DebugLinkState {
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)
#[derive(PartialEq,Clone,Default,Debug)]
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\
\rmnemonic_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.Bac\
kupTypeR\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\
\"\x0f\n\rDebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\
\x20\x01(\rR\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\
\x12\x12\n\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRe\
ad\x12\x18\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06leng\
th\x18\x02\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06me\
mory\x18\x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\
\x18\n\x07address\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\x06sector\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\
\x01\x20\x01(\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05w\
atch\x18\x01\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkRes\
etDebugEvents:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.s\
atoshilabs.trezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\
\"\x87\x01\n\x17DebugLinkGetPairingInfo\x12\x1d\n\nchannel_id\x18\x01\
\x20\x01(\x0cR\tchannelId\x12%\n\x0ehandshake_hash\x18\x02\x20\x01(\x0cR\
\rhandshakeHash\x12&\n\x0fnfc_secret_host\x18\x03\x20\x01(\x0cR\rnfcSecr\
etHost\"\xd2\x01\n\x14DebugLinkPairingInfo\x12\x1d\n\nchannel_id\x18\x01\
\x20\x01(\x0cR\tchannelId\x12%\n\x0ehandshake_hash\x18\x02\x20\x01(\x0cR\
\rhandshakeHash\x12&\n\x0fcode_entry_code\x18\x03\x20\x01(\rR\rcodeEntry\
Code\x12\x20\n\x0ccode_qr_code\x18\x04\x20\x01(\x0cR\ncodeQrCode\x12*\n\
\x11nfc_secret_trezor\x18\x05\x20\x01(\x0cR\x0fnfcSecretTrezor\"\x0f\n\r\
DebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\x20\x01(\r\
R\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\x12\x12\n\
\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRead\x12\x18\
\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06length\x18\x02\
\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06memory\x18\
\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
@ -3741,13 +4372,15 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor {
deps.push(super::messages_common::file_descriptor().clone());
deps.push(super::messages_management::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(DebugLinkLayout::generated_message_descriptor_data());
messages.push(DebugLinkReseedRandom::generated_message_descriptor_data());
messages.push(DebugLinkRecordScreen::generated_message_descriptor_data());
messages.push(DebugLinkGetState::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(DebugLinkLog::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)