mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-21 23:18:13 +00:00
MONOREPO MERGE python-trezor
This commit is contained in:
commit
37fe33fb4d
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -45,3 +45,6 @@
|
|||||||
path = legacy/vendor/QR-Code-generator
|
path = legacy/vendor/QR-Code-generator
|
||||||
url = https://github.com/nayuki/QR-Code-generator.git
|
url = https://github.com/nayuki/QR-Code-generator.git
|
||||||
ignore = untracked
|
ignore = untracked
|
||||||
|
[submodule "python/vendor/trezor-common"]
|
||||||
|
path = python/vendor/trezor-common
|
||||||
|
url = https://github.com/trezor/trezor-common.git
|
||||||
|
20
python/.gitignore
vendored
Normal file
20
python/.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
MANIFEST
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
/trezor.egg-info
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
*.bin
|
||||||
|
*.py.cache
|
||||||
|
/.tox
|
||||||
|
/.cache
|
||||||
|
/.pytest_cache
|
||||||
|
/.mypy_cache
|
||||||
|
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
/trezorlib/coins.json
|
||||||
|
/trezorlib/messages/*
|
57
python/.travis.yml
Normal file
57
python/.travis.yml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
language: python
|
||||||
|
|
||||||
|
# Runs jobs on container based infrastructure
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
# Saves pip downloads/wheels between builds
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.cache/pip
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- libudev-dev
|
||||||
|
- libusb-1.0-0-dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
PROTOBUF_VERSION=3.4.0
|
||||||
|
|
||||||
|
python:
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
|
|
||||||
|
# workaround for https://github.com/travis-ci/travis-ci/issues/9815
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python: 3.7
|
||||||
|
dist: xenial
|
||||||
|
sudo: true
|
||||||
|
|
||||||
|
install:
|
||||||
|
# Optimisation: build requirements as wheels, which get cached by Travis
|
||||||
|
- pip install "pip>=9.0" wheel # pip 9.0 understands `python_requires` constraints
|
||||||
|
- pip install "setuptools>=38" # setuptools >= 38 are capable of using prebuilt wheels
|
||||||
|
- pip install tox-travis
|
||||||
|
- pip install -r requirements-dev.txt
|
||||||
|
# protobuf-related dependencies
|
||||||
|
- curl -LO "https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip"
|
||||||
|
- unzip "protoc-${PROTOBUF_VERSION}-linux-x86_64.zip" -d protoc
|
||||||
|
- export PATH="$(pwd)/protoc/bin:$PATH"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- ./trigger-travis.sh
|
||||||
|
|
||||||
|
script:
|
||||||
|
- python setup.py install
|
||||||
|
- if [ $TRAVIS_PYTHON_VERSION != 3.5 ]; then make style_check; fi
|
||||||
|
- tox
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- http://ci-bot.satoshilabs.com:5000/travis
|
||||||
|
on_success: always
|
||||||
|
on_failure: always
|
||||||
|
on_start: always
|
19
python/AUTHORS
Normal file
19
python/AUTHORS
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
python-trezor is free software, created in 2012 and maintained by SatoshiLabs
|
||||||
|
as part of the Trezor project.
|
||||||
|
|
||||||
|
Over the years, many people have contributed to the project. Here is an incomplete
|
||||||
|
list of credits:
|
||||||
|
|
||||||
|
alepop <https://github.com/alepop>
|
||||||
|
Jan 'matejcik' Matějek <jan.matejek@satoshilabs.com>
|
||||||
|
Jan Pochyla <jan.pochyla@satoshilabs.com>
|
||||||
|
Jochen Hoenicke <hoenicke@gmail.com>
|
||||||
|
Karel Bílek <karel.bilek@satoshilabs.com>
|
||||||
|
Marek Palatinus <slush@satoshilabs.com>
|
||||||
|
mruddy <https://github.com/mruddy>
|
||||||
|
Pavol Rusnak <stick@satoshilabs.com>
|
||||||
|
Peter van Mourik <https://github.com/tyrion70>
|
||||||
|
Roman Zeyde <https://github.com/romanz>
|
||||||
|
Saleem Rashid <trezor@saleemrashid.com>
|
||||||
|
Tomáš Sušánka <tomas.susanka@satoshilabs.com>
|
||||||
|
ZuluCrypto <https://github.com/zulucrypto>
|
267
python/CHANGELOG.md
Normal file
267
python/CHANGELOG.md
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
_At the moment, the project does **not** adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). That is expected to change with version 1.0._
|
||||||
|
|
||||||
|
## [0.11.3] - Unreleased
|
||||||
|
|
||||||
|
[0.11.3]: https://github.com/trezor/python-trezor/compare/v0.11.2...master
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- trezorctl can now send ERC20 tokens
|
||||||
|
- trezorctl usb-reset will perform USB reset on devices in inconsistent state
|
||||||
|
- set-display-rotation command added for TT firmware 2.1.1
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Minimum firmware versions bumped to 1.8.0 and 2.1.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ethereum commands in trezorctl now work
|
||||||
|
|
||||||
|
## [0.11.2] - 2019-02-27
|
||||||
|
|
||||||
|
[0.11.2]: https://github.com/trezor/python-trezor/compare/v0.11.1...v0.11.2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- full support for bootloader 1.8.0 and relevant firmware upgrade functionality
|
||||||
|
- trezorctl: support fully offline signing JSON-encoded transaction data
|
||||||
|
- trezorctl: dry-run for firmware upgrade command
|
||||||
|
- client: new convenience function `get_default_client` for simple script usage
|
||||||
|
- Dash: support DIP-2 special inputs [#351]
|
||||||
|
- Ethereum: add get_public_key methods
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- coins with BIP-143 fork id (BCH, BTG) won't require prev_tx [#352]
|
||||||
|
- device recovery will restore U2F counter
|
||||||
|
- Cardano: change `network` to `protocol_magic`
|
||||||
|
- tests can run interactively when `INTERACT=1` environment variable is set
|
||||||
|
- protobuf: improved `to_dict` function
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- trezorctl: interactive signing with `sign-tx` is considered deprecated
|
||||||
|
|
||||||
|
## [0.11.1] - 2018-12-28
|
||||||
|
|
||||||
|
[0.11.1]: https://github.com/trezor/python-trezor/compare/v0.11.0...v0.11.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- crash when entering passphrase on device with Trezor T
|
||||||
|
- Qt widgets should only import QtCore [#349]
|
||||||
|
|
||||||
|
## [0.11.0] - 2018-12-06
|
||||||
|
|
||||||
|
[0.11.0]: https://github.com/trezor/python-trezor/compare/v0.10.2...v0.11.0
|
||||||
|
|
||||||
|
### Incompatible changes
|
||||||
|
|
||||||
|
- removed support for Python 3.3 and 3.4
|
||||||
|
- major refactor of `TrezorClient` and UI handling. Implementers must now provide a "UI" object instead of overriding callbacks [#307], [#314]
|
||||||
|
- protobuf classes now use a `get_fields()` method instead of `FIELDS` field [#312]
|
||||||
|
- all methods on `TrezorClient` class are now in separate modules and take a `TrezorClient` instance as argument [#276]
|
||||||
|
- mixin classes are also removed, you are not supposed to extend `TrezorClient` anymore
|
||||||
|
- `TrezorClientDebugLink` was moved to `debuglink` module
|
||||||
|
- changed signature of `trezorlib.btc.sign_tx`
|
||||||
|
- `@field` decorator was replaced by an argument to `@expect`
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- trezorlib now has a hardcoded check preventing use of outdated firmware versions [#283]
|
||||||
|
- Ripple support [#286]
|
||||||
|
- Zencash support [#287]
|
||||||
|
- Cardano support [#300]
|
||||||
|
- Ontology support [#301]
|
||||||
|
- Tezos support [#302]
|
||||||
|
- Capricoin support [#325]
|
||||||
|
- limited Monero support (can only get address/watch key, monerowallet is required for signing)
|
||||||
|
- support for input flow in tests makes it easier to control complex UI workflows [#314]
|
||||||
|
- `protobuf.dict_to_proto` can create a protobuf instance from a plain dict
|
||||||
|
- support for smarter methods in trezord 2.0.25 and up
|
||||||
|
- support for seedless setup
|
||||||
|
- trezorctl: firmware handling is greatly improved [#304], [#308]
|
||||||
|
- trezorctl: Bitcoin-like signing flow is more user-friendly
|
||||||
|
- `tx_api` now supports Blockbook backend servers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- better reporting for debuglink expected messages
|
||||||
|
- replaced Ed25519 module with a cleaner, optimized version
|
||||||
|
- further reorganization of transports makes them more robust when dependencies are missing
|
||||||
|
- codebase now follows [Black](https://github.com/ambv/black) code style
|
||||||
|
- in Qt modules, Qt5 is imported first [#315]
|
||||||
|
- `TxApiInsight` is just `TxApi`
|
||||||
|
- `device.reset` and `device.recover` now have reasonable defaults for all arguments
|
||||||
|
- protobuf classes are no longer part of the source distribution and must be compiled locally [#284]
|
||||||
|
- Stellar: addresses are always strings
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `set_tx_api` method on `TrezorClient` is replaced by an argument for `sign_tx`
|
||||||
|
- caching functionality of `TxApi` was moved to a separate test-support class
|
||||||
|
- Stellar: public key methods removed
|
||||||
|
- `EncryptMessage` and `DecryptMessage` actions are gone
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
|
||||||
|
- `TrezorClient` can now detect when a HID device is removed and a different one is plugged in on the same path
|
||||||
|
- trezorctl now works with Click 7.0 and considers "`_`" and "`-`" as same in command names [#314]
|
||||||
|
- bash completion fixed
|
||||||
|
- Stellar: several bugs in the XDR parser were fixed
|
||||||
|
|
||||||
|
## [0.10.2] - 2018-06-21
|
||||||
|
|
||||||
|
[0.10.2]: https://github.com/trezor/python-trezor/compare/v0.10.1...v0.10.2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `stellar_get_address` and `_public_key` functions support `show_display` parameter
|
||||||
|
- trezorctl: `stellar_get_address` and `_public_key` commands for the respective functionality
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- trezorctl: `list_coins` is removed because we no longer parse the relevant protobuf field
|
||||||
|
(and newer Trezor firmwares don't send it) [#277]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- test support module was not included in the release, so code relying on the deprecated `ckd_public` module would fail [#280]
|
||||||
|
|
||||||
|
## [0.10.1] - 2018-06-11
|
||||||
|
|
||||||
|
[0.10.1]: https://github.com/trezor/python-trezor/compare/v0.10.0...v0.10.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- previous release fails to build on Windows [#274]
|
||||||
|
|
||||||
|
## [0.10.0] - 2018-06-08
|
||||||
|
|
||||||
|
[0.10.0]: https://github.com/trezor/python-trezor/compare/v0.9.1...v0.10.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Lisk support [#197]
|
||||||
|
- Stellar support [#167], [#268]
|
||||||
|
- Wanchain support [#230]
|
||||||
|
- support for "auto lock delay" feature
|
||||||
|
- `TrezorClient` takes an additional argument `state` that allows reusing the previously entered passphrase [#241]
|
||||||
|
- USB transports mention udev rules in exception messages [#245]
|
||||||
|
- `log.enable_debug_output` function turns on wire logging, instead of having to use `TrezorClientVerbose`
|
||||||
|
- BIP32 paths now support `123h` in addition to `123'` to indicate hardening
|
||||||
|
- trezorctl: `-p` now supports prefix search for device path [#226]
|
||||||
|
- trezorctl: smarter handling of firmware updates [#242], [#269]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- reorganized transports and moved into their own `transport` submodule
|
||||||
|
- protobuf messages and coins info is now regenerated at build time from the `trezor-common` repository [#248]
|
||||||
|
- renamed `ed25519raw` to `_ed25519` to indicate its privateness
|
||||||
|
- renamed `ed25519cosi` to `cosi` and expanded its API
|
||||||
|
- protobuf messages are now logged through Python's `logging` facility instead of custom printing through `VerboseWireMixin`
|
||||||
|
- `client.format_protobuf` is moved to `protobuf.format_message`
|
||||||
|
- `tools.Hash` is renamed to `tools.btc_hash`
|
||||||
|
- `coins` module `coins_txapi` is renamed to `tx_api`.
|
||||||
|
`coins_slip44` is renamed to `slip44`.
|
||||||
|
- build: stricter flake8 checks
|
||||||
|
- build: split requirements to separate files
|
||||||
|
- tests: unified finding test device, while respecting `TREZOR_PATH` env variable.
|
||||||
|
- tests: auto-skip appropriately marked tests based on Trezor device version
|
||||||
|
- tests: only show wire output when run with `-v`
|
||||||
|
- tests: allow running `xfail`ed tests selectively based on `pytest.ini`
|
||||||
|
- docs: updated README with clearer install instructions [#185]
|
||||||
|
- docs: switched changelog to Keep a Changelog format [#94]
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `ckd_public` is only maintained in `tests.support` submodule and considered private
|
||||||
|
- `TrezorClient.expand_path` is moved to plain function `tools.parse_path`
|
||||||
|
- `TrezorDevice` is deprecated in favor of `transport.enumerate_devices` and `transport.get_transport`
|
||||||
|
- XPUB-related handling in `tools` is slated for removal
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- most Python 2 compatibility constructs are gone [#229]
|
||||||
|
- `TrezorClientVerbose` and `VerboseWireMixin` is removed
|
||||||
|
- specific `tx_api.TxApi*` classes removed in favor of `coins.tx_api`
|
||||||
|
- `client.PRIME_DERIVATION_FLAG` is removed in favor of `tools.HARDENED_FLAG` and `tools.H_()`
|
||||||
|
- hard dependency on Ethereum libraries and HIDAPI is changed into extras that need to be
|
||||||
|
specified explicitly. Require `trezor[hidapi]` or `trezor[ethereum]` to get them.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- WebUSB enumeration returning bad devices on Windows 10 [#223]
|
||||||
|
- `sign_tx` operation sending empty address string [#237]
|
||||||
|
- Wrongly formatted Ethereum signatures [#236]
|
||||||
|
- protobuf layer would wrongly encode signed integers [#249], [#250]
|
||||||
|
- protobuf pretty-printing broken on Python 3.4 [#256]
|
||||||
|
- trezorctl: Matrix recovery on Windows wouldn't allow backspace [#207]
|
||||||
|
- aes_encfs_getpass.py: fixed Python 3 bug [#169]
|
||||||
|
|
||||||
|
## [0.9.1] - 2018-03-05
|
||||||
|
|
||||||
|
[0.9.1]: https://github.com/trezor/python-trezor/compare/v0.9.0...v0.9.1
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- proper support for Trezor model T
|
||||||
|
- support for Monacoin
|
||||||
|
- improvements to `trezorctl`:
|
||||||
|
- add pretty-printing of features and protobuf debug dumps (fixes [#199])
|
||||||
|
- support `TREZOR_PATH` environment variable to preselect a Trezor device.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- gradually dropping Python 2 compatibility (pypi package will now be marked as Python 3 only)
|
||||||
|
|
||||||
|
[#94]: https://github.com/trezor/python-trezor/issues/94
|
||||||
|
[#167]: https://github.com/trezor/python-trezor/issues/167
|
||||||
|
[#169]: https://github.com/trezor/python-trezor/issues/169
|
||||||
|
[#185]: https://github.com/trezor/python-trezor/issues/185
|
||||||
|
[#197]: https://github.com/trezor/python-trezor/issues/197
|
||||||
|
[#199]: https://github.com/trezor/python-trezor/issues/199
|
||||||
|
[#207]: https://github.com/trezor/python-trezor/issues/207
|
||||||
|
[#223]: https://github.com/trezor/python-trezor/issues/223
|
||||||
|
[#226]: https://github.com/trezor/python-trezor/issues/226
|
||||||
|
[#229]: https://github.com/trezor/python-trezor/issues/229
|
||||||
|
[#230]: https://github.com/trezor/python-trezor/issues/230
|
||||||
|
[#236]: https://github.com/trezor/python-trezor/issues/236
|
||||||
|
[#237]: https://github.com/trezor/python-trezor/issues/237
|
||||||
|
[#241]: https://github.com/trezor/python-trezor/issues/241
|
||||||
|
[#242]: https://github.com/trezor/python-trezor/issues/242
|
||||||
|
[#245]: https://github.com/trezor/python-trezor/issues/245
|
||||||
|
[#248]: https://github.com/trezor/python-trezor/issues/248
|
||||||
|
[#249]: https://github.com/trezor/python-trezor/issues/249
|
||||||
|
[#250]: https://github.com/trezor/python-trezor/issues/250
|
||||||
|
[#256]: https://github.com/trezor/python-trezor/issues/256
|
||||||
|
[#268]: https://github.com/trezor/python-trezor/issues/268
|
||||||
|
[#269]: https://github.com/trezor/python-trezor/issues/269
|
||||||
|
[#274]: https://github.com/trezor/python-trezor/issues/274
|
||||||
|
[#276]: https://github.com/trezor/python-trezor/issues/276
|
||||||
|
[#277]: https://github.com/trezor/python-trezor/issues/277
|
||||||
|
[#280]: https://github.com/trezor/python-trezor/issues/280
|
||||||
|
[#283]: https://github.com/trezor/python-trezor/issues/283
|
||||||
|
[#284]: https://github.com/trezor/python-trezor/issues/284
|
||||||
|
[#286]: https://github.com/trezor/python-trezor/issues/286
|
||||||
|
[#287]: https://github.com/trezor/python-trezor/issues/287
|
||||||
|
[#300]: https://github.com/trezor/python-trezor/issues/300
|
||||||
|
[#301]: https://github.com/trezor/python-trezor/issues/301
|
||||||
|
[#302]: https://github.com/trezor/python-trezor/issues/302
|
||||||
|
[#304]: https://github.com/trezor/python-trezor/issues/304
|
||||||
|
[#307]: https://github.com/trezor/python-trezor/issues/307
|
||||||
|
[#308]: https://github.com/trezor/python-trezor/issues/308
|
||||||
|
[#312]: https://github.com/trezor/python-trezor/issues/312
|
||||||
|
[#314]: https://github.com/trezor/python-trezor/issues/314
|
||||||
|
[#315]: https://github.com/trezor/python-trezor/issues/315
|
||||||
|
[#325]: https://github.com/trezor/python-trezor/issues/325
|
||||||
|
[#349]: https://github.com/trezor/python-trezor/issues/349
|
||||||
|
[#351]: https://github.com/trezor/python-trezor/issues/351
|
||||||
|
[#352]: https://github.com/trezor/python-trezor/issues/352
|
165
python/COPYING
Normal file
165
python/COPYING
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
9
python/MANIFEST.in
Normal file
9
python/MANIFEST.in
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
recursive-include bash_completion.d *.sh
|
||||||
|
include tools/*
|
||||||
|
recursive-include trezorlib *
|
||||||
|
|
||||||
|
recursive-include vendor/trezor-common *
|
||||||
|
exclude vendor/trezor-common/.*
|
||||||
|
|
||||||
|
include AUTHORS README.md COPYING CHANGELOG.md
|
||||||
|
include requirements*.txt
|
58
python/Makefile
Normal file
58
python/Makefile
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
PYTHON=python3
|
||||||
|
SETUP=$(PYTHON) setup.py
|
||||||
|
|
||||||
|
EXCLUDES=.vscode
|
||||||
|
STYLE_TARGETS=trezorlib trezorctl setup.py
|
||||||
|
EXCLUDE_TARGETS=trezorlib/messages
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(SETUP) build
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(SETUP) install
|
||||||
|
|
||||||
|
dist: clean
|
||||||
|
$(SETUP) sdist
|
||||||
|
$(SETUP) bdist_wheel
|
||||||
|
|
||||||
|
clean: clean-generated clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||||
|
|
||||||
|
clean-generated: ## remove generated files
|
||||||
|
rm -f trezorlib/messages/*.py
|
||||||
|
rm -f trezorlib/coins.json
|
||||||
|
|
||||||
|
clean-build: ## remove build artifacts
|
||||||
|
rm -fr build/
|
||||||
|
rm -fr dist/
|
||||||
|
rm -fr .eggs/
|
||||||
|
find . -name '*.egg-info' -exec rm -fr {} +
|
||||||
|
find . -name '*.egg' -exec rm -f {} +
|
||||||
|
|
||||||
|
clean-pyc: ## remove Python file artifacts
|
||||||
|
find . -name '*.pyc' -exec rm -f {} +
|
||||||
|
find . -name '*.pyo' -exec rm -f {} +
|
||||||
|
find . -name '*~' -exec rm -f {} +
|
||||||
|
find . -name '__pycache__' -exec rm -fr {} +
|
||||||
|
|
||||||
|
clean-test: ## remove test and coverage artifacts
|
||||||
|
rm -fr .tox/
|
||||||
|
rm -f .coverage
|
||||||
|
rm -fr htmlcov/
|
||||||
|
rm -fr .pytest_cache
|
||||||
|
|
||||||
|
git-clean:
|
||||||
|
git clean -dfx -e $(EXCLUDES)
|
||||||
|
|
||||||
|
style:
|
||||||
|
black $(STYLE_TARGETS)
|
||||||
|
isort --apply --recursive $(STYLE_TARGETS) --skip-glob "*/$(EXCLUDE_TARGETS)/*"
|
||||||
|
autoflake -i --remove-all-unused-imports -r $(STYLE_TARGETS) --exclude "$(EXCLUDE_TARGETS)"
|
||||||
|
|
||||||
|
style_check:
|
||||||
|
black --check $(STYLE_TARGETS)
|
||||||
|
isort --diff --check-only --recursive $(STYLE_TARGETS) --skip-glob "*/$(EXCLUDE_TARGETS)/*"
|
||||||
|
flake8
|
||||||
|
|
||||||
|
.PHONY: all build install clean style style_check git-clean clean-generated clean-build clean-pyc clean-test
|
172
python/README.md
Normal file
172
python/README.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# python-trezor
|
||||||
|
|
||||||
|
[![image](https://travis-ci.org/trezor/python-trezor.svg?branch=master)](https://travis-ci.org/trezor/python-trezor) [![repology](https://repology.org/badge/tiny-repos/python:trezor.svg)](https://repology.org/metapackage/python:trezor) [![image](https://badges.gitter.im/trezor/community.svg)](https://gitter.im/trezor/community)
|
||||||
|
|
||||||
|
Python library and commandline client for communicating with TREZOR
|
||||||
|
Hardware Wallet
|
||||||
|
|
||||||
|
See <https://trezor.io> for more information
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Python-trezor requires Python 3.5 or higher, and libusb 1.0. The easiest
|
||||||
|
way to install it is with `pip`. The rest of this guide assumes you have
|
||||||
|
a working `pip`; if not, you can refer to [this
|
||||||
|
guide](https://packaging.python.org/tutorials/installing-packages/).
|
||||||
|
|
||||||
|
### Quick installation
|
||||||
|
|
||||||
|
On a typical Linux / Mac / BSD system, you already have all you need.
|
||||||
|
Install `trezor` with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip3 install --upgrade setuptools
|
||||||
|
pip3 install trezor
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows, you also need to install
|
||||||
|
[libusb](https://github.com/libusb/libusb/wiki/Windows) and the
|
||||||
|
appropriate [drivers](https://zadig.akeo.ie/). This is, unfortunately, a
|
||||||
|
topic bigger than this README.
|
||||||
|
|
||||||
|
### Older Trezor One support
|
||||||
|
|
||||||
|
If your Trezor One is on firmware **1.6.3** or older, you will need HIDAPI support
|
||||||
|
for it to be recognized. That requires additional packages.
|
||||||
|
|
||||||
|
#### Debian / Ubuntu
|
||||||
|
|
||||||
|
On a Debian or Ubuntu based system, you can install these:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt-get install python3-dev python3-pip cython3 libusb-1.0-0-dev libudev-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
On a Windows based system, you can install these (for more info on choco, refer to [this](https://chocolatey.org/install)):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
choco install vcbuildtools python3 protoc
|
||||||
|
refreshenv
|
||||||
|
pip3 install protobuf
|
||||||
|
```
|
||||||
|
|
||||||
|
When installing the trezor library, you need to specify that you want
|
||||||
|
`hidapi`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip3 install --upgrade setuptools
|
||||||
|
pip3 install trezor[hidapi]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ethereum support
|
||||||
|
|
||||||
|
Ethereum requires additional python packages. Instead of
|
||||||
|
`pip3 install trezor`, specify `pip3 install trezor[ethereum]`.
|
||||||
|
|
||||||
|
You can combine it with the above, to get both HIDAPI and Ethereum
|
||||||
|
support:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip3 install trezor[ethereum,hidapi]
|
||||||
|
```
|
||||||
|
|
||||||
|
### FreeBSD
|
||||||
|
|
||||||
|
On FreeBSD you can install the packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pkg install security/py-trezor
|
||||||
|
```
|
||||||
|
|
||||||
|
or build via ports:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /usr/ports/security/py-trezor
|
||||||
|
make install clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building from source
|
||||||
|
|
||||||
|
Sometimes you might need to install the latest-and-greatest unreleased version
|
||||||
|
straight from GitHub. You will need some prerequisites first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt-get install protobuf-compiler protobuf-dev
|
||||||
|
pip3 install protobuf
|
||||||
|
```
|
||||||
|
|
||||||
|
If you just need to install the package, you can use pip again:
|
||||||
|
```sh
|
||||||
|
pip3 install git+https://github.com/trezor/python-trezor
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to work on the sources, make a local clone:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/trezor/python-trezor
|
||||||
|
cd python-trezor
|
||||||
|
python3 setup.py prebuild
|
||||||
|
python3 setup.py develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command line client (trezorctl)
|
||||||
|
|
||||||
|
The included `trezorctl` python script can perform various tasks such as
|
||||||
|
changing setting in the Trezor, signing transactions, retrieving account
|
||||||
|
info and addresses. See the [docs/](docs/) sub folder for detailed
|
||||||
|
examples and options.
|
||||||
|
|
||||||
|
NOTE: An older version of the `trezorctl` command is [available for
|
||||||
|
Debian Stretch](https://packages.debian.org/en/stretch/python-trezor)
|
||||||
|
(and comes pre-installed on [Tails OS](https://tails.boum.org/)).
|
||||||
|
|
||||||
|
## Python Library
|
||||||
|
|
||||||
|
You can use this python library to interact with a Bitcoin Trezor and
|
||||||
|
use its capabilities in your application. See examples here in the
|
||||||
|
[tools/](tools/) sub folder.
|
||||||
|
|
||||||
|
## PIN Entering
|
||||||
|
|
||||||
|
When you are asked for PIN, you have to enter scrambled PIN. Follow the
|
||||||
|
numbers shown on TREZOR display and enter the their positions using the
|
||||||
|
numeric keyboard mapping:
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|---|---|---|
|
||||||
|
| 7 | 8 | 9 |
|
||||||
|
| 4 | 5 | 6 |
|
||||||
|
| 1 | 2 | 3 |
|
||||||
|
|
||||||
|
Example: your PIN is **1234** and TREZOR is displaying the following:
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|---|---|---|
|
||||||
|
| 2 | 8 | 3 |
|
||||||
|
| 5 | 4 | 6 |
|
||||||
|
| 7 | 9 | 1 |
|
||||||
|
|
||||||
|
You have to enter: **3795**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Python-trezor pulls coins info and protobuf messages from
|
||||||
|
[trezor-common](https://github.com/trezor/trezor-common) repository. If
|
||||||
|
you are developing new features for Trezor, you will want to start
|
||||||
|
there. Once your changes are accepted to `trezor-common`, you can make a
|
||||||
|
PR against this repository. Don't forget to update the submodule with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git submodule update --init --remote
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, rebuild the protobuf messages and get `coins.json` by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 setup.py prebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
To get support for BTC-like coins, these steps are enough and no further
|
||||||
|
changes to the library are necessary.
|
21
python/bash_completion.d/trezorctl.sh
Normal file
21
python/bash_completion.d/trezorctl.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
_trezorctl()
|
||||||
|
{
|
||||||
|
export TREZORCTL_COMPLETION_CACHE
|
||||||
|
local cur prev cmds base
|
||||||
|
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
|
||||||
|
if [ -z "$TREZORCTL_COMPLETION_CACHE" ]; then
|
||||||
|
help_output=$(trezorctl --help | grep '^ [a-z]' | awk '{ print $1 }')
|
||||||
|
export TREZORCTL_COMPLETION_CACHE="$help_output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmds="$TREZORCTL_COMPLETION_CACHE"
|
||||||
|
|
||||||
|
COMPREPLY=($(compgen -W "${cmds}" -- ${cur}))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _trezorctl trezorctl
|
115
python/docs/EXAMPLES.rst
Normal file
115
python/docs/EXAMPLES.rst
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
Examples demonstrating how to use trezorctl
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Show all available `options <OPTIONS.rst>`_:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl --help
|
||||||
|
|
||||||
|
|
||||||
|
Retrieve features, settings and coin types supported by your device:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-features
|
||||||
|
|
||||||
|
|
||||||
|
Bitcoin examples
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Get first receiving address of first account for Bitcoin (Legacy / non-SegWit):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-address --coin Bitcoin --script-type address --address "m/44'/0'/0'/0/0"
|
||||||
|
|
||||||
|
Get first receiving address of first account for Bitcoin (SegWit-in-P2SH):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-address --coin Bitcoin --script-type p2shsegwit --address "m/49'/0'/0'/0/0"
|
||||||
|
|
||||||
|
Get first receiving address of first account for Bitcoin (Bech32 native SegWit P2WPKH):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-address --coin Bitcoin --script-type segwit --address "m/84'/0'/0'/0/0"
|
||||||
|
|
||||||
|
Get Legacy Bitcoin ``xpub`` (can be used to create a watch-only wallet):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-public-node --coin Bitcoin --address "m/44'/0'/0'"
|
||||||
|
|
||||||
|
|
||||||
|
Transaction signing
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can use ``trezorctl`` to sign a transaction without it automatically being broadcast to the Bitcoin network.
|
||||||
|
|
||||||
|
You will need the following pieces of info:
|
||||||
|
|
||||||
|
1) Transaction ID containing the Output we want to spend (aka ``prevhash`` or ``a5ea715a...d201e64e`` in example below).
|
||||||
|
2) Index number of the Output being spent from the above tx (aka ``previndex`` or ``0`` in example below).
|
||||||
|
3) BIP32 path to the Node which can spend the above UTXO (eg ``Bitcoin/0'/0/0`` for the first).
|
||||||
|
4) Destination address where you want to send funds (eg ``3M8XGFBKwkf7miBzpkU3x2DoWwAVrD1mhk`` below).
|
||||||
|
5) Amount to send in satoshis - ``91305`` in the example below (multiply BTC amount 0.00091305 by 100,000,000).
|
||||||
|
6) Expected fee (``0.00019695`` BTC in example below). Note: the miner receives all satoshis left unspent from a transaction. If you want to receive some change, you need to send it to an address you own (otherwise it will go to miner). Fee is not needed below, we just want it as a sanity check.
|
||||||
|
|
||||||
|
There are many ways to retrieve the info above: from a watch-only wallet in Bitcoin Core, https://coinb.in (`screenshot <sign_tx-coinb.in.png>`_) etc. The easiest way is using the Trezor online wallet: https://beta-wallet.trezor.io
|
||||||
|
|
||||||
|
After authenticating, open the "Send" tab, fill-out all details, then open the "Show transaction details" menu to see the info needed above (`screenshot <sign_tx-trezor.io.png>`_). Once you have the required details, you can then perform the transaction signing using ``trezorctl`` as shown in the example below:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl sign-tx -c Bitcoin
|
||||||
|
|
||||||
|
Input (prevhash:previndex, empty to move on): a5ea715aa99ca30516f3af6f622dfe7399d883d49ad74b1fe33fdf73d201e64e:0
|
||||||
|
Node path to sign with (e.g.- Bitcoin/0'/0/0): Bitcoin/0'/0/0
|
||||||
|
|
||||||
|
Input (prevhash:previndex, empty to move on):
|
||||||
|
|
||||||
|
Pay to address (empty to move on): 3M8XGFBKwkf7miBzpkU3x2DoWwAVrD1mhk
|
||||||
|
Amount (in satoshis): 91305
|
||||||
|
|
||||||
|
Pay to address (empty to move on):
|
||||||
|
Passphrase required:
|
||||||
|
|
||||||
|
Confirm your Passphrase:
|
||||||
|
|
||||||
|
RECEIVED PART OF SERIALIZED TX (152 BYTES)
|
||||||
|
RECEIVED PART OF SERIALIZED TX (37 BYTES)
|
||||||
|
SIGNED IN 52.538 SECONDS, CALLED 10 MESSAGES, 189 BYTES
|
||||||
|
|
||||||
|
Signed Transaction:
|
||||||
|
01000000014ee601d273df3fe31f4bd79ad483d89973fe2d626faff31605a39ca95a71eaa5000000006a47304402206386a0ad0f0b196d375a0805eee2aebe4644032c2998aaf00e43ce68a293986702202ad25964844657e10130f81201b7d87eb8047cf0c09dfdcbbe68a1a732e80ded012103b375a0dd50c8dbc4a6156a55e31274ee0537191e1bc824a09278a220fafba2dbffffffff01a96401000000000017a914d53d47ccd1579b93c284e9caf3c81f3f417871698700000000
|
||||||
|
|
||||||
|
Use the following form to broadcast it to the network:
|
||||||
|
https://btc-bitcore1.trezor.io/tx/send
|
||||||
|
|
||||||
|
|
||||||
|
The signed transaction text can then be inspected in Electrum (`screenshot <sign_tx-electrum2.png>`_), `coinb.in <https://coinb.in/?verify=01000000014ee601d273df3fe31f4bd79ad483d89973fe2d626faff31605a39ca95a71eaa5000000006a47304402206386a0ad0f0b196d375a0805eee2aebe4644032c2998aaf00e43ce68a293986702202ad25964844657e10130f81201b7d87eb8047cf0c09dfdcbbe68a1a732e80ded012103b375a0dd50c8dbc4a6156a55e31274ee0537191e1bc824a09278a220fafba2dbffffffff01a96401000000000017a914d53d47ccd1579b93c284e9caf3c81f3f417871698700000000#verify>`_ or another tool. If all info is correct, you can then broadcast the tx to the Bitcoin network via the URL provided by ``trezorctl`` or Electrum (Tools → Load transaction → From text. Here is a `screenshot <sign_tx-electrum1.png>`_). TIP: Electrum will only show the transaction fee if you previously imported the spending address (eg ``16ijWp48xn8hj6deD5ZHSJcgNjtYbpiki8`` from example tx above). Also, the final tx size (and therefore satoshis / byte) might be slightly different than the estimate shown on beta-wallet.trezor.io
|
||||||
|
|
||||||
|
The final broadcast and mined transaction can be seen here: https://blockchain.info/tx/270684c14be85efec9adafa50339fd120658381ed2300b9207d0a0df2a5f0bf9
|
||||||
|
|
||||||
|
|
||||||
|
Litecoin examples
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Get first receiving address of first account for Litecoin (SegWit-in-P2SH):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-address --coin Litecoin --script-type p2shsegwit --address "m/49'/2'/0'/0/0"
|
||||||
|
|
||||||
|
Get first receiving address of first account for Litecoin (Bech32 native SegWit P2WPKH):
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl get-address --coin Litecoin --script-type segwit --address "m/84'/2'/0'/0/0"
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
1. Bech32 native SegWit encoded addresses require `Trezor Firmware v1.6.0 <https://github.com/trezor/trezor-mcu/releases>`_ or later.
|
83
python/docs/OPTIONS.rst
Normal file
83
python/docs/OPTIONS.rst
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
Commandline options for trezorctl
|
||||||
|
=================================
|
||||||
|
|
||||||
|
See `EXAMPLES.rst <EXAMPLES.rst>`_ for examples on how to use.
|
||||||
|
|
||||||
|
Use the following command to see all options:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
trezorctl --help
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Usage: trezorctl [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-p, --path TEXT Select device by specific path.
|
||||||
|
-v, --verbose Show communication messages.
|
||||||
|
-j, --json Print result as JSON object
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
backup-device Perform device seed backup.
|
||||||
|
cardano-get-address Get Cardano address.
|
||||||
|
cardano-get-public-key Get Cardano public key.
|
||||||
|
cardano-sign-tx Sign Cardano transaction.
|
||||||
|
change-pin Change new PIN or remove existing.
|
||||||
|
clear-session Clear session (remove cached PIN, passphrase, etc.).
|
||||||
|
cosi-commit Ask device to commit to CoSi signing.
|
||||||
|
cosi-sign Ask device to sign using CoSi.
|
||||||
|
decrypt-keyvalue Decrypt value by given key and path.
|
||||||
|
disable-passphrase Disable passphrase.
|
||||||
|
enable-passphrase Enable passphrase.
|
||||||
|
encrypt-keyvalue Encrypt value by given key and path.
|
||||||
|
ethereum-get-address Get Ethereum address in hex encoding.
|
||||||
|
ethereum-sign-message Sign message with Ethereum address.
|
||||||
|
ethereum-sign-tx Sign (and optionally publish) Ethereum transaction.
|
||||||
|
ethereum-verify-message Verify message signed with Ethereum address.
|
||||||
|
firmware-update Upload new firmware to device.
|
||||||
|
get-address Get address for specified path.
|
||||||
|
get-entropy Get example entropy.
|
||||||
|
get-features Retrieve device features and settings.
|
||||||
|
get-public-node Get public node of given path.
|
||||||
|
lisk-get-address Get Lisk address for specified path.
|
||||||
|
lisk-get-public-key Get Lisk public key for specified path.
|
||||||
|
lisk-sign-message Sign message with Lisk address.
|
||||||
|
lisk-sign-tx Sign Lisk transaction.
|
||||||
|
lisk-verify-message Verify message signed with Lisk address.
|
||||||
|
list List connected TREZOR devices.
|
||||||
|
load-device Load custom configuration to the device.
|
||||||
|
monero-get-address Get Monero address for specified path.
|
||||||
|
monero-get-watch-key Get Monero watch key for specified path.
|
||||||
|
nem-get-address Get NEM address for specified path.
|
||||||
|
nem-sign-tx Sign (and optionally broadcast) NEM transaction.
|
||||||
|
ontology-get-address Get Ontology address for specified path.
|
||||||
|
ontology-get-public-key Get Ontology public key for specified path.
|
||||||
|
ontology-sign-ont-id-add-attributes
|
||||||
|
Sign Ontology ONT ID Attributes adding.
|
||||||
|
ontology-sign-ont-id-register Sign Ontology ONT ID Registration.
|
||||||
|
ontology-sign-transfer Sign Ontology transfer.
|
||||||
|
ontology-sign-withdraw-ong Sign Ontology withdraw Ong.
|
||||||
|
ping Send ping message.
|
||||||
|
recovery-device Start safe recovery workflow.
|
||||||
|
reset-device Perform device setup and generate new seed.
|
||||||
|
ripple-get-address Get Ripple address
|
||||||
|
ripple-sign-tx Sign Ripple transaction
|
||||||
|
self-test Perform a self-test.
|
||||||
|
set-auto-lock-delay Set auto-lock delay (in seconds).
|
||||||
|
set-flags Set device flags.
|
||||||
|
set-homescreen Set new homescreen.
|
||||||
|
set-label Set new device label.
|
||||||
|
set-passphrase-source Set passphrase source.
|
||||||
|
set-u2f-counter Set U2F counter.
|
||||||
|
sign-message Sign message using address of given path.
|
||||||
|
sign-tx Sign transaction.
|
||||||
|
stellar-get-address Get Stellar public address
|
||||||
|
stellar-sign-transaction Sign a base64-encoded transaction envelope
|
||||||
|
tezos-get-address Get Tezos address for specified path.
|
||||||
|
tezos-get-public-key Get Tezos public key.
|
||||||
|
tezos-sign-tx Sign Tezos transaction.
|
||||||
|
verify-message Verify message.
|
||||||
|
version Show version of trezorctl/trezorlib.
|
||||||
|
wipe-device Reset device to factory defaults and remove all private data.
|
5
python/docs/README.rst
Normal file
5
python/docs/README.rst
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Documentation for trezorctl commandline client
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
* `EXAMPLES.rst <EXAMPLES.rst>`_ - Examples demonstrating how to use trezorctl
|
||||||
|
* `OPTIONS.rst <OPTIONS.rst>`_ - Commandline options for trezorctl
|
BIN
python/docs/sign_tx-coinb.in.png
Normal file
BIN
python/docs/sign_tx-coinb.in.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
python/docs/sign_tx-electrum1.png
Normal file
BIN
python/docs/sign_tx-electrum1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
python/docs/sign_tx-electrum2.png
Normal file
BIN
python/docs/sign_tx-electrum2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
python/docs/sign_tx-trezor.io.png
Normal file
BIN
python/docs/sign_tx-trezor.io.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
4
python/helper-scripts/README.md
Normal file
4
python/helper-scripts/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
These scripts automate some tasks related to release process.
|
||||||
|
|
||||||
|
* __`relicence.py`__ rewrites licence headers in all non-empty Python files
|
||||||
|
* __`linkify-changelog.py`__ generates Markdown links to github issues/PRs in changelog
|
57
python/helper-scripts/bump-required-fw-versions.py
Executable file
57
python/helper-scripts/bump-required-fw-versions.py
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
RELEASES_URL = "https://beta-wallet.trezor.io/data/firmware/{}/releases.json"
|
||||||
|
MODELS = ("1", "T")
|
||||||
|
|
||||||
|
FILENAME = os.path.join(os.path.dirname(__file__), "..", "trezorlib", "__init__.py")
|
||||||
|
START_LINE = "MINIMUM_FIRMWARE_VERSION = {\n"
|
||||||
|
END_LINE = "}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def version_str(vtuple):
|
||||||
|
return ".".join(map(str, vtuple))
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_releases(model):
|
||||||
|
version = model
|
||||||
|
if model == "T":
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
url = RELEASES_URL.format(version)
|
||||||
|
releases = requests.get(url).json()
|
||||||
|
releases.sort(key=lambda r: r["version"], reverse=True)
|
||||||
|
return releases
|
||||||
|
|
||||||
|
|
||||||
|
def find_latest_required(model):
|
||||||
|
releases = fetch_releases(model)
|
||||||
|
return next(r for r in releases if r["required"])
|
||||||
|
|
||||||
|
|
||||||
|
with open(FILENAME, "r+") as f:
|
||||||
|
output = []
|
||||||
|
line = None
|
||||||
|
# copy up to & incl START_LINE
|
||||||
|
while line != START_LINE:
|
||||||
|
line = next(f)
|
||||||
|
output.append(line)
|
||||||
|
# throw away until END_LINE
|
||||||
|
while line != END_LINE:
|
||||||
|
line = next(f)
|
||||||
|
# append models
|
||||||
|
for model in MODELS:
|
||||||
|
rel = find_latest_required(model)
|
||||||
|
version_tuple = tuple(rel["version"])
|
||||||
|
line = f' "{model}": {version_tuple!r},\n'
|
||||||
|
output.append(line)
|
||||||
|
output.append(END_LINE)
|
||||||
|
# finish reading file
|
||||||
|
for line in f:
|
||||||
|
output.append(line)
|
||||||
|
|
||||||
|
f.seek(0)
|
||||||
|
f.truncate(0)
|
||||||
|
for line in output:
|
||||||
|
f.write(line)
|
27
python/helper-scripts/linkify-changelog.py
Executable file
27
python/helper-scripts/linkify-changelog.py
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
LINK_RE = re.compile(r"\[#(\d+)\]")
|
||||||
|
ISSUE_URL = "https://github.com/trezor/python-trezor/issues/"
|
||||||
|
|
||||||
|
CHANGELOG = os.path.dirname(__file__) + "/../CHANGELOG.md"
|
||||||
|
|
||||||
|
changelog_entries = set()
|
||||||
|
result_lines = []
|
||||||
|
|
||||||
|
with open(CHANGELOG, "r+") as changelog:
|
||||||
|
for line in changelog:
|
||||||
|
if ISSUE_URL in line:
|
||||||
|
break
|
||||||
|
for n in LINK_RE.findall(line):
|
||||||
|
changelog_entries.add(int(n))
|
||||||
|
result_lines.append(line)
|
||||||
|
|
||||||
|
changelog.seek(0)
|
||||||
|
changelog.truncate(0)
|
||||||
|
for line in result_lines:
|
||||||
|
changelog.write(line)
|
||||||
|
for issue in sorted(changelog_entries):
|
||||||
|
changelog.write(f"[#{issue}]: {ISSUE_URL}{issue}\n")
|
51
python/helper-scripts/relicence.py
Executable file
51
python/helper-scripts/relicence.py
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
LICENSE_NOTICE = """\
|
||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXCLUDE_FILES = ["trezorlib/__init__.py", "trezorlib/_ed25519.py"]
|
||||||
|
|
||||||
|
|
||||||
|
def one_file(fp):
|
||||||
|
lines = list(fp)
|
||||||
|
new = lines[:]
|
||||||
|
while new and new[0][0] == "#":
|
||||||
|
new.pop(0)
|
||||||
|
|
||||||
|
while new and new[0].strip() == "":
|
||||||
|
new.pop(0)
|
||||||
|
|
||||||
|
data = "".join([LICENSE_NOTICE] + new)
|
||||||
|
|
||||||
|
fp.seek(0)
|
||||||
|
fp.write(data)
|
||||||
|
fp.truncate()
|
||||||
|
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
for fn in glob.glob("trezorlib/**/*.py", recursive=True):
|
||||||
|
if fn in EXCLUDE_FILES:
|
||||||
|
continue
|
||||||
|
statinfo = os.stat(fn)
|
||||||
|
if statinfo.st_size == 0:
|
||||||
|
continue
|
||||||
|
with open(fn, "r+") as fp:
|
||||||
|
one_file(fp)
|
8
python/requirements-dev.txt
Normal file
8
python/requirements-dev.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
-r requirements-optional.txt
|
||||||
|
pytest>=3.6
|
||||||
|
flake8
|
||||||
|
protobuf
|
||||||
|
isort==4.3.10
|
||||||
|
black; python_version >= "3.6"
|
||||||
|
autoflake>=1.2
|
3
python/requirements-optional.txt
Normal file
3
python/requirements-optional.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
hidapi >= 0.7.99.post20
|
||||||
|
rlp >= 1.1.0
|
||||||
|
web3 >= 4.8
|
8
python/requirements.txt
Normal file
8
python/requirements.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ecdsa>=0.9
|
||||||
|
mnemonic>=0.17
|
||||||
|
requests>=2.4.0
|
||||||
|
click>=7,<8
|
||||||
|
pyblake2>=0.9.3
|
||||||
|
libusb1>=1.6.4
|
||||||
|
construct>=2.9
|
||||||
|
typing_extensions>=3.6
|
36
python/setup.cfg
Normal file
36
python/setup.cfg
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[flake8]
|
||||||
|
filename =
|
||||||
|
*.py,
|
||||||
|
./trezorctl
|
||||||
|
exclude =
|
||||||
|
.tox/,
|
||||||
|
build/,
|
||||||
|
dist/,
|
||||||
|
vendor/,
|
||||||
|
trezorlib/messages/__init__.py
|
||||||
|
ignore =
|
||||||
|
# E203 whitespace before ':'
|
||||||
|
E203,
|
||||||
|
# E221: multiple spaces before operator
|
||||||
|
E221,
|
||||||
|
# E241: multiple spaces after ':'
|
||||||
|
E241,
|
||||||
|
# E402: module level import not at top of file
|
||||||
|
E402,
|
||||||
|
# E501: line too long
|
||||||
|
E501,
|
||||||
|
# E741 ambiguous variable name
|
||||||
|
E741,
|
||||||
|
# W503: line break before binary operator
|
||||||
|
W503
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = True
|
||||||
|
force_grid_wrap = 0
|
||||||
|
combine_as_imports = True
|
||||||
|
line_length = 88
|
||||||
|
not_skip=__init__.py
|
||||||
|
known_first_party=trezorlib
|
||||||
|
known_third_party=hidapi, rlp, ethjsonrpc, ecdsa, mnemonic, requests, click, pyblake2, \
|
||||||
|
usb, construct, pytest
|
148
python/setup.py
Executable file
148
python/setup.py
Executable file
@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from distutils.errors import DistutilsError
|
||||||
|
|
||||||
|
from setuptools import Command, find_packages, setup
|
||||||
|
from setuptools.command.build_py import build_py
|
||||||
|
from setuptools.command.develop import develop
|
||||||
|
|
||||||
|
install_requires = [
|
||||||
|
"setuptools>=19.0",
|
||||||
|
"ecdsa>=0.9",
|
||||||
|
"mnemonic>=0.17",
|
||||||
|
"requests>=2.4.0",
|
||||||
|
"click>=7,<8",
|
||||||
|
"pyblake2>=0.9.3",
|
||||||
|
"libusb1>=1.6.4",
|
||||||
|
"construct>=2.9",
|
||||||
|
"typing_extensions>=3.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
CWD = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
TREZOR_COMMON = os.path.join(CWD, "vendor", "trezor-common")
|
||||||
|
|
||||||
|
|
||||||
|
def read(*path):
|
||||||
|
filename = os.path.join(CWD, *path)
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def find_version():
|
||||||
|
version_file = read("trezorlib", "__init__.py")
|
||||||
|
version_match = re.search(r"^__version__ = \"(.*)\"$", version_file, re.M)
|
||||||
|
if version_match:
|
||||||
|
return version_match.group(1)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Version string not found")
|
||||||
|
|
||||||
|
|
||||||
|
def build_coins_json(dst):
|
||||||
|
TOOLS_PATH = os.path.join(TREZOR_COMMON, "tools")
|
||||||
|
sys.path.insert(0, TOOLS_PATH)
|
||||||
|
import coin_info
|
||||||
|
|
||||||
|
coins = coin_info.coin_info().bitcoin
|
||||||
|
support = coin_info.support_info(coins)
|
||||||
|
for coin in coins:
|
||||||
|
coin["support"] = support[coin["key"]]
|
||||||
|
|
||||||
|
with open(dst, "w") as f:
|
||||||
|
json.dump(coins, f, indent=2, sort_keys=True)
|
||||||
|
|
||||||
|
del sys.path[0]
|
||||||
|
|
||||||
|
|
||||||
|
class PrebuildCommand(Command):
|
||||||
|
description = "update vendored files (coins.json, protobuf messages)"
|
||||||
|
user_options = []
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# check for existence of the submodule directory
|
||||||
|
common_defs = os.path.join(TREZOR_COMMON, "defs")
|
||||||
|
if not os.path.exists(common_defs):
|
||||||
|
raise DistutilsError(
|
||||||
|
"trezor-common submodule seems to be missing.\n"
|
||||||
|
+ "Use 'git submodule update --init' to retrieve it."
|
||||||
|
)
|
||||||
|
|
||||||
|
# generate and copy coins.json to the tree
|
||||||
|
coins_json = os.path.join(CWD, "trezorlib", "coins.json")
|
||||||
|
build_coins_json(coins_json)
|
||||||
|
|
||||||
|
# regenerate messages
|
||||||
|
try:
|
||||||
|
proto_srcs = glob.glob(os.path.join(TREZOR_COMMON, "protob", "*.proto"))
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
os.path.join(TREZOR_COMMON, "protob", "pb2py"),
|
||||||
|
"-o",
|
||||||
|
os.path.join(CWD, "trezorlib", "messages"),
|
||||||
|
"-P",
|
||||||
|
"..protobuf",
|
||||||
|
]
|
||||||
|
+ proto_srcs
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise DistutilsError(
|
||||||
|
"Generating protobuf failed. Make sure you have 'protoc' in your PATH."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_prebuild(cls):
|
||||||
|
"""Patch a setuptools command to depend on `prebuild`"""
|
||||||
|
orig_run = cls.run
|
||||||
|
|
||||||
|
def new_run(self):
|
||||||
|
self.run_command("prebuild")
|
||||||
|
orig_run(self)
|
||||||
|
|
||||||
|
cls.run = new_run
|
||||||
|
|
||||||
|
|
||||||
|
_patch_prebuild(build_py)
|
||||||
|
_patch_prebuild(develop)
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="trezor",
|
||||||
|
version=find_version(),
|
||||||
|
author="TREZOR",
|
||||||
|
author_email="info@trezor.io",
|
||||||
|
license="LGPLv3",
|
||||||
|
description="Python library for communicating with TREZOR Hardware Wallet",
|
||||||
|
long_description="{}\n\n{}".format(read("README.md"), read("CHANGELOG.md")),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/trezor/python-trezor",
|
||||||
|
packages=find_packages(),
|
||||||
|
package_data={"trezorlib": ["coins.json"]},
|
||||||
|
scripts=["trezorctl"],
|
||||||
|
install_requires=install_requires,
|
||||||
|
extras_require={
|
||||||
|
"hidapi": ["hidapi>=0.7.99.post20"],
|
||||||
|
"ethereum": ["rlp>=1.1.0", "web3>=4.8"],
|
||||||
|
},
|
||||||
|
python_requires=">=3.5",
|
||||||
|
include_package_data=True,
|
||||||
|
zip_safe=False,
|
||||||
|
classifiers=[
|
||||||
|
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
|
"Operating System :: MacOS :: MacOS X",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
],
|
||||||
|
cmdclass={"prebuild": PrebuildCommand},
|
||||||
|
)
|
9
python/shell.nix
Normal file
9
python/shell.nix
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
with import <nixpkgs> {};
|
||||||
|
|
||||||
|
let
|
||||||
|
myPython = python3.withPackages(p: [p.pytest p.black p.isort p.flake8 p.requests p.mnemonic p.construct p.pyblake2 p.mock p.ecdsa p.click p.libusb1 p.protobuf p.typing-extensions]);
|
||||||
|
in
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
name = "python-trezor-dev";
|
||||||
|
buildInputs = [ myPython autoflake protobuf ];
|
||||||
|
}
|
84
python/tools/deserialize_tx.py
Executable file
84
python/tools/deserialize_tx.py
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import construct as c
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("This tool requires Construct. Install it with 'pip install Construct'.\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from construct import this, len_
|
||||||
|
|
||||||
|
if os.isatty(sys.stdin.fileno()):
|
||||||
|
tx_hex = input("Enter transaction in hex format: ")
|
||||||
|
else:
|
||||||
|
tx_hex = sys.stdin.read().strip()
|
||||||
|
|
||||||
|
tx_bin = bytes.fromhex(tx_hex)
|
||||||
|
|
||||||
|
|
||||||
|
CompactUintStruct = c.Struct(
|
||||||
|
"base" / c.Int8ul,
|
||||||
|
"ext" / c.Switch(this.base, {0xfd: c.Int16ul, 0xfe: c.Int32ul, 0xff: c.Int64ul}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CompactUintAdapter(c.Adapter):
|
||||||
|
def _encode(self, obj, context, path):
|
||||||
|
if obj < 0xfd:
|
||||||
|
return {"base": obj}
|
||||||
|
if obj < 2 ** 16:
|
||||||
|
return {"base": 0xfd, "ext": obj}
|
||||||
|
if obj < 2 ** 32:
|
||||||
|
return {"base": 0xfe, "ext": obj}
|
||||||
|
if obj < 2 ** 64:
|
||||||
|
return {"base": 0xff, "ext": obj}
|
||||||
|
raise ValueError("Value too big for compact uint")
|
||||||
|
|
||||||
|
def _decode(self, obj, context, path):
|
||||||
|
return obj["ext"] or obj["base"]
|
||||||
|
|
||||||
|
|
||||||
|
class ConstFlag(c.Adapter):
|
||||||
|
def __init__(self, const):
|
||||||
|
self.const = const
|
||||||
|
super().__init__(c.Optional(c.Const(const)))
|
||||||
|
|
||||||
|
def _encode(self, obj, context, path):
|
||||||
|
return self.const if obj else None
|
||||||
|
|
||||||
|
def _decode(self, obj, context, path):
|
||||||
|
return obj is not None
|
||||||
|
|
||||||
|
|
||||||
|
CompactUint = CompactUintAdapter(CompactUintStruct)
|
||||||
|
|
||||||
|
TxInput = c.Struct(
|
||||||
|
"tx" / c.Bytes(32),
|
||||||
|
"index" / c.Int32ul,
|
||||||
|
# TODO coinbase tx
|
||||||
|
"script" / c.Prefixed(CompactUint, c.GreedyBytes),
|
||||||
|
"sequence" / c.Int32ul,
|
||||||
|
)
|
||||||
|
|
||||||
|
TxOutput = c.Struct(
|
||||||
|
"value" / c.Int64ul,
|
||||||
|
"pk_script" / c.Prefixed(CompactUint, c.GreedyBytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
StackItem = c.Prefixed(CompactUint, c.GreedyBytes)
|
||||||
|
TxInputWitness = c.PrefixedArray(CompactUint, StackItem)
|
||||||
|
|
||||||
|
Transaction = c.Struct(
|
||||||
|
"version" / c.Int32ul,
|
||||||
|
"segwit" / ConstFlag(b"\x00\x01"),
|
||||||
|
"inputs" / c.PrefixedArray(CompactUint, TxInput),
|
||||||
|
"outputs" / c.PrefixedArray(CompactUint, TxOutput),
|
||||||
|
"witness" / c.If(this.segwit, TxInputWitness[len_(this.inputs)]),
|
||||||
|
"lock_time" / c.Int32ul,
|
||||||
|
c.Terminated,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(Transaction.parse(tx_bin))
|
138
python/tools/encfs_aes_getpass.py
Executable file
138
python/tools/encfs_aes_getpass.py
Executable file
@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Use TREZOR as a hardware key for opening EncFS filesystem!
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
encfs --standard --extpass=./encfs_aes_getpass.py ~/.crypt ~/crypt
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import trezorlib
|
||||||
|
|
||||||
|
version_tuple = tuple(map(int, trezorlib.__version__.split(".")))
|
||||||
|
if not (0, 11) <= version_tuple < (0, 12):
|
||||||
|
raise RuntimeError("trezorlib version mismatch (0.11.x is required)")
|
||||||
|
|
||||||
|
from trezorlib.client import TrezorClient
|
||||||
|
from trezorlib.transport import enumerate_devices
|
||||||
|
from trezorlib.ui import ClickUI
|
||||||
|
|
||||||
|
import trezorlib.misc
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_devices():
|
||||||
|
devices = enumerate_devices()
|
||||||
|
while not len(devices):
|
||||||
|
sys.stderr.write("Please connect TREZOR to computer and press Enter...")
|
||||||
|
input()
|
||||||
|
devices = enumerate_devices()
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def choose_device(devices):
|
||||||
|
if not len(devices):
|
||||||
|
raise RuntimeError("No TREZOR connected!")
|
||||||
|
|
||||||
|
if len(devices) == 1:
|
||||||
|
try:
|
||||||
|
return devices[0]
|
||||||
|
except IOError:
|
||||||
|
raise RuntimeError("Device is currently in use")
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
sys.stderr.write("----------------------------\n")
|
||||||
|
sys.stderr.write("Available devices:\n")
|
||||||
|
for d in devices:
|
||||||
|
try:
|
||||||
|
client = TrezorClient(d, ui=ClickUI())
|
||||||
|
except IOError:
|
||||||
|
sys.stderr.write("[-] <device is currently in use>\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if client.features.label:
|
||||||
|
sys.stderr.write("[%d] %s\n" % (i, client.features.label))
|
||||||
|
else:
|
||||||
|
sys.stderr.write("[%d] <no label>\n" % i)
|
||||||
|
client.close()
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
sys.stderr.write("----------------------------\n")
|
||||||
|
sys.stderr.write("Please choose device to use:")
|
||||||
|
|
||||||
|
try:
|
||||||
|
device_id = int(input())
|
||||||
|
return devices[device_id]
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("Invalid choice, exiting...")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
if "encfs_root" not in os.environ:
|
||||||
|
sys.stderr.write(
|
||||||
|
"\nThis is not a standalone script and is not meant to be run independently.\n"
|
||||||
|
)
|
||||||
|
sys.stderr.write(
|
||||||
|
"\nUsage: encfs --standard --extpass=./encfs_aes_getpass.py ~/.crypt ~/crypt\n"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
devices = wait_for_devices()
|
||||||
|
transport = choose_device(devices)
|
||||||
|
client = TrezorClient(transport, ui=ClickUI())
|
||||||
|
|
||||||
|
rootdir = os.environ["encfs_root"] # Read "man encfs" for more
|
||||||
|
passw_file = os.path.join(rootdir, "password.dat")
|
||||||
|
|
||||||
|
if not os.path.exists(passw_file):
|
||||||
|
# New encfs drive, let's generate password
|
||||||
|
|
||||||
|
sys.stderr.write("Please provide label for new drive: ")
|
||||||
|
label = input()
|
||||||
|
|
||||||
|
sys.stderr.write("Computer asked TREZOR for new strong password.\n")
|
||||||
|
|
||||||
|
# 32 bytes, good for AES
|
||||||
|
trezor_entropy = trezorlib.misc.get_entropy(client, 32)
|
||||||
|
urandom_entropy = os.urandom(32)
|
||||||
|
passw = hashlib.sha256(trezor_entropy + urandom_entropy).digest()
|
||||||
|
|
||||||
|
if len(passw) != 32:
|
||||||
|
raise ValueError("32 bytes password expected")
|
||||||
|
|
||||||
|
bip32_path = [10, 0]
|
||||||
|
passw_encrypted = trezorlib.misc.encrypt_keyvalue(
|
||||||
|
client, bip32_path, label, passw, False, True
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"label": label,
|
||||||
|
"bip32_path": bip32_path,
|
||||||
|
"password_encrypted_hex": passw_encrypted.hex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
json.dump(data, open(passw_file, "w"))
|
||||||
|
|
||||||
|
# Let's load password
|
||||||
|
data = json.load(open(passw_file, "r"))
|
||||||
|
|
||||||
|
passw = trezorlib.misc.decrypt_keyvalue(
|
||||||
|
client,
|
||||||
|
data["bip32_path"],
|
||||||
|
data["label"],
|
||||||
|
bytes.fromhex(data["password_encrypted_hex"]),
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(passw)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
22
python/tools/helloworld.py
Executable file
22
python/tools/helloworld.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from trezorlib.client import get_default_client
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
from trezorlib import btc
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Use first connected device
|
||||||
|
client = get_default_client()
|
||||||
|
|
||||||
|
# Print out TREZOR's features and settings
|
||||||
|
print(client.features)
|
||||||
|
|
||||||
|
# Get the first address of first BIP44 account
|
||||||
|
# (should be the same address as shown in wallet.trezor.io)
|
||||||
|
bip32_path = parse_path("44'/0'/0'/0/0")
|
||||||
|
address = btc.get_address(client, "Bitcoin", bip32_path, True)
|
||||||
|
print("Bitcoin address:", address)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
49
python/tools/mem_flashblock.py
Executable file
49
python/tools/mem_flashblock.py
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from trezorlib.debuglink import DebugLink
|
||||||
|
from trezorlib.transport import enumerate_devices
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
sectoraddrs = [0x8000000, 0x8004000, 0x8008000, 0x800c000,
|
||||||
|
0x8010000, 0x8020000, 0x8040000, 0x8060000,
|
||||||
|
0x8080000, 0x80a0000, 0x80c0000, 0x80f0000]
|
||||||
|
sectorlens = [0x4000, 0x4000, 0x4000, 0x4000,
|
||||||
|
0x8000, 0x10000, 0x10000, 0x10000,
|
||||||
|
0x10000, 0x10000, 0x10000, 0x10000]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
def find_debug():
|
||||||
|
for device in enumerate_devices():
|
||||||
|
try:
|
||||||
|
debug_transport = device.find_debug()
|
||||||
|
debug = DebugLink(debug_transport, auto_interact=False)
|
||||||
|
debug.open()
|
||||||
|
return debug
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("No suitable Trezor device found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
debug = find_debug()
|
||||||
|
|
||||||
|
sector = int(sys.argv[1])
|
||||||
|
f = open(sys.argv[2], "rb")
|
||||||
|
content = f.read(sectorlens[sector])
|
||||||
|
if len(content) != sectorlens[sector]:
|
||||||
|
print("Not enough bytes in file")
|
||||||
|
return
|
||||||
|
|
||||||
|
debug.flash_erase(sector)
|
||||||
|
step = 0x400
|
||||||
|
for offset in range(0, sectorlens[sector], step):
|
||||||
|
debug.memory_write(
|
||||||
|
sectoraddrs[sector] + offset, content[offset : offset + step], flash=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
46
python/tools/mem_read.py
Executable file
46
python/tools/mem_read.py
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from trezorlib.debuglink import DebugLink
|
||||||
|
from trezorlib.transport import enumerate_devices
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# usage examples
|
||||||
|
# read entire bootloader: ./mem_read.py 8000000 8000
|
||||||
|
# read initial stack pointer: ./mem_read.py 8000000 4
|
||||||
|
# an entire bootloader can be later disassembled with:
|
||||||
|
# arm-none-eabi-objdump -D -b binary -m arm -M force-thumb memory.dat
|
||||||
|
# note that in order for this to work, your trezor device must
|
||||||
|
# be running a firmware that was built with debug link enabled
|
||||||
|
|
||||||
|
|
||||||
|
def find_debug():
|
||||||
|
for device in enumerate_devices():
|
||||||
|
try:
|
||||||
|
debug_transport = device.find_debug()
|
||||||
|
debug = DebugLink(debug_transport, auto_interact=False)
|
||||||
|
debug.open()
|
||||||
|
return debug
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("No suitable Trezor device found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
debug = find_debug()
|
||||||
|
|
||||||
|
arg1 = int(sys.argv[1], 16)
|
||||||
|
arg2 = int(sys.argv[2], 16)
|
||||||
|
step = 0x400 if arg2 >= 0x400 else arg2
|
||||||
|
|
||||||
|
f = open("memory.dat", "wb")
|
||||||
|
|
||||||
|
for addr in range(arg1, arg1 + arg2, step):
|
||||||
|
mem = debug.memory_read(addr, step)
|
||||||
|
f.write(mem)
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
27
python/tools/mem_write.py
Executable file
27
python/tools/mem_write.py
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from trezorlib.debuglink import DebugLink
|
||||||
|
from trezorlib.transport import enumerate_devices
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def find_debug():
|
||||||
|
for device in enumerate_devices():
|
||||||
|
try:
|
||||||
|
debug_transport = device.find_debug()
|
||||||
|
debug = DebugLink(debug_transport, auto_interact=False)
|
||||||
|
debug.open()
|
||||||
|
return debug
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("No suitable Trezor device found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
debug = find_debug()
|
||||||
|
debug.memory_write(int(sys.argv[1], 16), bytes.fromhex(sys.argv[2]), flash=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
72
python/tools/mnemonic_check.py
Executable file
72
python/tools/mnemonic_check.py
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import mnemonic
|
||||||
|
|
||||||
|
__doc__ = '''
|
||||||
|
Use this script to cross-check that TREZOR generated valid
|
||||||
|
mnemonic sentence for given internal (TREZOR-generated)
|
||||||
|
and external (computer-generated) entropy.
|
||||||
|
|
||||||
|
Keep in mind that you're entering secret information to this script.
|
||||||
|
Leaking of these information may lead to stealing your bitcoins
|
||||||
|
from your wallet! We strongly recommend to run this script only on
|
||||||
|
highly secured computer (ideally live linux distribution
|
||||||
|
without an internet connection).
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def generate_entropy(strength, internal_entropy, external_entropy):
|
||||||
|
'''
|
||||||
|
strength - length of produced seed. One of 128, 192, 256
|
||||||
|
random - binary stream of random data from external HRNG
|
||||||
|
'''
|
||||||
|
if strength not in (128, 192, 256):
|
||||||
|
raise ValueError("Invalid strength")
|
||||||
|
|
||||||
|
if not internal_entropy:
|
||||||
|
raise ValueError("Internal entropy is not provided")
|
||||||
|
|
||||||
|
if len(internal_entropy) < 32:
|
||||||
|
raise ValueError("Internal entropy too short")
|
||||||
|
|
||||||
|
if not external_entropy:
|
||||||
|
raise ValueError("External entropy is not provided")
|
||||||
|
|
||||||
|
if len(external_entropy) < 32:
|
||||||
|
raise ValueError("External entropy too short")
|
||||||
|
|
||||||
|
entropy = hashlib.sha256(internal_entropy + external_entropy).digest()
|
||||||
|
entropy_stripped = entropy[:strength // 8]
|
||||||
|
|
||||||
|
if len(entropy_stripped) * 8 != strength:
|
||||||
|
raise ValueError("Entropy length mismatch")
|
||||||
|
|
||||||
|
return entropy_stripped
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(__doc__)
|
||||||
|
|
||||||
|
comp = bytes.fromhex(input("Please enter computer-generated entropy (in hex): ").strip())
|
||||||
|
trzr = bytes.fromhex(input("Please enter TREZOR-generated entropy (in hex): ").strip())
|
||||||
|
word_count = int(input("How many words your mnemonic has? "))
|
||||||
|
|
||||||
|
strength = word_count * 32 // 3
|
||||||
|
|
||||||
|
entropy = generate_entropy(strength, trzr, comp)
|
||||||
|
|
||||||
|
words = mnemonic.Mnemonic('english').to_mnemonic(entropy)
|
||||||
|
if not mnemonic.Mnemonic('english').check(words):
|
||||||
|
print("Mnemonic is invalid")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(words.split(' ')) != word_count:
|
||||||
|
print("Mnemonic length mismatch!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Generated mnemonic is:", words)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
179
python/tools/pwd_reader.py
Executable file
179
python/tools/pwd_reader.py
Executable file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from trezorlib import misc, ui
|
||||||
|
from trezorlib.client import TrezorClient
|
||||||
|
from trezorlib.transport import get_transport
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
|
||||||
|
# Return path by BIP-32
|
||||||
|
BIP32_PATH = parse_path("10016h/0")
|
||||||
|
|
||||||
|
|
||||||
|
# Deriving master key
|
||||||
|
def getMasterKey(client):
|
||||||
|
bip32_path = BIP32_PATH
|
||||||
|
ENC_KEY = 'Activate TREZOR Password Manager?'
|
||||||
|
ENC_VALUE = bytes.fromhex('2d650551248d792eabf628f451200d7f51cb63e46aadcbb1038aacb05e8c8aee2d650551248d792eabf628f451200d7f51cb63e46aadcbb1038aacb05e8c8aee')
|
||||||
|
key = misc.encrypt_keyvalue(
|
||||||
|
client,
|
||||||
|
bip32_path,
|
||||||
|
ENC_KEY,
|
||||||
|
ENC_VALUE,
|
||||||
|
True,
|
||||||
|
True
|
||||||
|
)
|
||||||
|
return key.hex()
|
||||||
|
|
||||||
|
|
||||||
|
# Deriving file name and encryption key
|
||||||
|
def getFileEncKey(key):
|
||||||
|
filekey, enckey = key[:len(key) // 2], key[len(key) // 2:]
|
||||||
|
FILENAME_MESS = b'5f91add3fa1c3c76e90c90a3bd0999e2bd7833d06a483fe884ee60397aca277a'
|
||||||
|
digest = hmac.new(str.encode(filekey), FILENAME_MESS, hashlib.sha256).hexdigest()
|
||||||
|
filename = digest + '.pswd'
|
||||||
|
return [filename, filekey, enckey]
|
||||||
|
|
||||||
|
|
||||||
|
# File level decryption and file reading
|
||||||
|
def decryptStorage(path, key):
|
||||||
|
cipherkey = bytes.fromhex(key)
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
iv = f.read(12)
|
||||||
|
tag = f.read(16)
|
||||||
|
cipher = Cipher(algorithms.AES(cipherkey), modes.GCM(iv, tag), backend=default_backend())
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
data = ''
|
||||||
|
while True:
|
||||||
|
block = f.read(16)
|
||||||
|
# data are not authenticated yet
|
||||||
|
if block:
|
||||||
|
data = data + decryptor.update(block).decode()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# throws exception when the tag is wrong
|
||||||
|
data = data + decryptor.finalize().decode()
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
def decryptEntryValue(nonce, val):
|
||||||
|
cipherkey = bytes.fromhex(nonce)
|
||||||
|
iv = val[:12]
|
||||||
|
tag = val[12:28]
|
||||||
|
cipher = Cipher(algorithms.AES(cipherkey), modes.GCM(iv, tag), backend=default_backend())
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
data = ''
|
||||||
|
inputData = val[28:]
|
||||||
|
while True:
|
||||||
|
block = inputData[:16]
|
||||||
|
inputData = inputData[16:]
|
||||||
|
if block:
|
||||||
|
data = data + decryptor.update(block).decode()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# throws exception when the tag is wrong
|
||||||
|
data = data + decryptor.finalize().decode()
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
# Decrypt give entry nonce
|
||||||
|
def getDecryptedNonce(client, entry):
|
||||||
|
print()
|
||||||
|
print('Waiting for TREZOR input ...')
|
||||||
|
print()
|
||||||
|
if 'item' in entry:
|
||||||
|
item = entry['item']
|
||||||
|
else:
|
||||||
|
item = entry['title']
|
||||||
|
|
||||||
|
pr = urlparse(item)
|
||||||
|
if pr.scheme and pr.netloc:
|
||||||
|
item = pr.netloc
|
||||||
|
|
||||||
|
ENC_KEY = 'Unlock %s for user %s?' % (item, entry['username'])
|
||||||
|
ENC_VALUE = entry['nonce']
|
||||||
|
decrypted_nonce = misc.decrypt_keyvalue(
|
||||||
|
client,
|
||||||
|
BIP32_PATH,
|
||||||
|
ENC_KEY,
|
||||||
|
bytes.fromhex(ENC_VALUE),
|
||||||
|
False,
|
||||||
|
True
|
||||||
|
)
|
||||||
|
return decrypted_nonce.hex()
|
||||||
|
|
||||||
|
|
||||||
|
# Pretty print of list
|
||||||
|
def printEntries(entries):
|
||||||
|
print('Password entries')
|
||||||
|
print('================')
|
||||||
|
print()
|
||||||
|
for k, v in entries.items():
|
||||||
|
print('Entry id: #%s' % k)
|
||||||
|
print('-------------')
|
||||||
|
for kk, vv in v.items():
|
||||||
|
if kk in ['nonce', 'safe_note', 'password']:
|
||||||
|
continue # skip these fields
|
||||||
|
print('*', kk, ': ', vv)
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
transport = get_transport()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
client = TrezorClient(transport=transport, ui=ui.ClickUI())
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('Confirm operation on TREZOR')
|
||||||
|
print()
|
||||||
|
|
||||||
|
masterKey = getMasterKey(client)
|
||||||
|
# print('master key:', masterKey)
|
||||||
|
|
||||||
|
fileName = getFileEncKey(masterKey)[0]
|
||||||
|
# print('file name:', fileName)
|
||||||
|
|
||||||
|
home = os.path.expanduser('~')
|
||||||
|
path = os.path.join(home, 'Dropbox', 'Apps', 'TREZOR Password Manager')
|
||||||
|
# print('path to file:', path)
|
||||||
|
|
||||||
|
encKey = getFileEncKey(masterKey)[2]
|
||||||
|
# print('enckey:', encKey)
|
||||||
|
|
||||||
|
full_path = os.path.join(path, fileName)
|
||||||
|
parsed_json = decryptStorage(full_path, encKey)
|
||||||
|
|
||||||
|
# list entries
|
||||||
|
entries = parsed_json['entries']
|
||||||
|
printEntries(entries)
|
||||||
|
|
||||||
|
entry_id = input('Select entry number to decrypt: ')
|
||||||
|
entry_id = str(entry_id)
|
||||||
|
|
||||||
|
plain_nonce = getDecryptedNonce(client, entries[entry_id])
|
||||||
|
|
||||||
|
pwdArr = entries[entry_id]['password']['data']
|
||||||
|
pwdHex = ''.join([hex(x)[2:].zfill(2) for x in pwdArr])
|
||||||
|
print('password: ', decryptEntryValue(plain_nonce, bytes.fromhex(pwdHex)))
|
||||||
|
|
||||||
|
safeNoteArr = entries[entry_id]['safe_note']['data']
|
||||||
|
safeNoteHex = ''.join([hex(x)[2:].zfill(2) for x in safeNoteArr])
|
||||||
|
print('safe_note:', decryptEntryValue(plain_nonce, bytes.fromhex(safeNoteHex)))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
34
python/tools/rng_entropy_collector.py
Executable file
34
python/tools/rng_entropy_collector.py
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# example usage: ./rng_entropy_collector.py stm32_rng_1.dat 1048576
|
||||||
|
# note: for reading large amounts of entropy, compile a firmware
|
||||||
|
# that has DEBUG_RNG == 1 as that will disable the user button
|
||||||
|
# push confirmation
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from trezorlib import misc, ui
|
||||||
|
from trezorlib.client import TrezorClient
|
||||||
|
from trezorlib.transport import get_transport
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
client = TrezorClient(get_transport(), ui=ui.ClickUI())
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
arg1 = sys.argv[1] # output file
|
||||||
|
arg2 = int(sys.argv[2], 10) # total number of how many bytes of entropy to read
|
||||||
|
step = 1024 if arg2 >= 1024 else arg2 # trezor will only return 1KB at a time
|
||||||
|
|
||||||
|
with io.open(arg1, 'wb') as f:
|
||||||
|
for i in range(0, arg2, step):
|
||||||
|
entropy = misc.get_entropy(client, step)
|
||||||
|
f.write(entropy)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
101
python/tools/trezor-otp.py
Executable file
101
python/tools/trezor-otp.py
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
from trezorlib.client import TrezorClient
|
||||||
|
from trezorlib.misc import decrypt_keyvalue, encrypt_keyvalue
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
from trezorlib.transport import get_transport
|
||||||
|
from trezorlib.ui import ClickUI
|
||||||
|
|
||||||
|
BIP32_PATH = parse_path("10016h/0")
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(type, domain, secret):
|
||||||
|
transport = get_transport()
|
||||||
|
client = TrezorClient(transport, ClickUI())
|
||||||
|
dom = type.upper() + ": " + domain
|
||||||
|
enc = encrypt_keyvalue(client, BIP32_PATH, dom, secret.encode(), False, True)
|
||||||
|
client.close()
|
||||||
|
return enc.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(type, domain, secret):
|
||||||
|
transport = get_transport()
|
||||||
|
client = TrezorClient(transport, ClickUI())
|
||||||
|
dom = type.upper() + ": " + domain
|
||||||
|
dec = decrypt_keyvalue(client, BIP32_PATH, dom, secret, False, True)
|
||||||
|
client.close()
|
||||||
|
return dec
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
XDG_CONFIG_HOME = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||||
|
os.makedirs(XDG_CONFIG_HOME, exist_ok=True)
|
||||||
|
self.filename = XDG_CONFIG_HOME + "/trezor-otp.ini"
|
||||||
|
self.config = configparser.ConfigParser()
|
||||||
|
self.config.read(self.filename)
|
||||||
|
|
||||||
|
def add(self, domain, secret, type="totp"):
|
||||||
|
self.config[domain] = {}
|
||||||
|
self.config[domain]["secret"] = encrypt(type, domain, secret)
|
||||||
|
self.config[domain]["type"] = type
|
||||||
|
if type == "hotp":
|
||||||
|
self.config[domain]["counter"] = "0"
|
||||||
|
with open(self.filename, "w") as f:
|
||||||
|
self.config.write(f)
|
||||||
|
|
||||||
|
def get(self, domain):
|
||||||
|
s = self.config[domain]
|
||||||
|
if s["type"] == "hotp":
|
||||||
|
s["counter"] = str(int(s["counter"]) + 1)
|
||||||
|
with open(self.filename, "w") as f:
|
||||||
|
self.config.write(f)
|
||||||
|
secret = decrypt(s["type"], domain, bytes.fromhex(s["secret"]))
|
||||||
|
if s["type"] == "totp":
|
||||||
|
return pyotp.TOTP(secret).now()
|
||||||
|
if s["type"] == "hotp":
|
||||||
|
c = int(s["counter"])
|
||||||
|
return pyotp.HOTP(secret).at(c)
|
||||||
|
return ValueError("unknown domain or type")
|
||||||
|
|
||||||
|
|
||||||
|
def add():
|
||||||
|
c = Config()
|
||||||
|
domain = input("domain: ")
|
||||||
|
while True:
|
||||||
|
secret = input("secret: ")
|
||||||
|
if re.match(r"^[A-Z2-7]{16}$", secret):
|
||||||
|
break
|
||||||
|
print("invalid secret")
|
||||||
|
while True:
|
||||||
|
type = input("type (t=totp h=hotp): ")
|
||||||
|
if type in ("t", "h"):
|
||||||
|
break
|
||||||
|
print("invalid type")
|
||||||
|
c.add(domain, secret, type + "otp")
|
||||||
|
print("Entry added")
|
||||||
|
|
||||||
|
|
||||||
|
def get(domain):
|
||||||
|
c = Config()
|
||||||
|
s = c.get(domain)
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: trezor-otp.py [add|domain]")
|
||||||
|
sys.exit(1)
|
||||||
|
if sys.argv[1] == "add":
|
||||||
|
add()
|
||||||
|
else:
|
||||||
|
get(sys.argv[1])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
18
python/tox.ini
Normal file
18
python/tox.ini
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[tox]
|
||||||
|
envlist =
|
||||||
|
py35,
|
||||||
|
py36,
|
||||||
|
py37,
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
deps =
|
||||||
|
-rrequirements-dev.txt
|
||||||
|
commands =
|
||||||
|
# Generate local files
|
||||||
|
python setup.py build
|
||||||
|
# Working in the local directory, try to compile all bytecode
|
||||||
|
python -m compileall trezorlib/
|
||||||
|
# From installed version, smoke-test trezorctl
|
||||||
|
trezorctl --help
|
||||||
|
# Run non-device-dependent tests from installed version
|
||||||
|
python -E -m pytest --pyarg trezorlib.tests.unit_tests
|
1989
python/trezorctl
Executable file
1989
python/trezorctl
Executable file
File diff suppressed because it is too large
Load Diff
8
python/trezorlib/__init__.py
Normal file
8
python/trezorlib/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
__version__ = "0.11.3"
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
MINIMUM_FIRMWARE_VERSION = {
|
||||||
|
"1": (1, 8, 0),
|
||||||
|
"T": (2, 1, 0),
|
||||||
|
}
|
||||||
|
# fmt: on
|
299
python/trezorlib/_ed25519.py
Normal file
299
python/trezorlib/_ed25519.py
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# ed25519.py - Optimized version of the reference implementation of Ed25519
|
||||||
|
# downloaded from https://github.com/pyca/ed25519
|
||||||
|
#
|
||||||
|
# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to>
|
||||||
|
# 2013 by Donald Stufft <donald@stufft.io>
|
||||||
|
# 2013 by Alex Gaynor <alex.gaynor@gmail.com>
|
||||||
|
# 2013 by Greg Price <price@mit.edu>
|
||||||
|
#
|
||||||
|
# To the extent possible under law, the author(s) have dedicated all copyright
|
||||||
|
# and related and neighboring rights to this software to the public domain
|
||||||
|
# worldwide. This software is distributed without any warranty.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the CC0 Public Domain Dedication along
|
||||||
|
# with this software. If not, see
|
||||||
|
# <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
NB: This code is not safe for use with secret keys or secret data.
|
||||||
|
The only safe use of this code is for verifying signatures on public messages.
|
||||||
|
|
||||||
|
Functions for computing the public key of a secret key and for signing
|
||||||
|
a message are included, namely publickey_unsafe and signature_unsafe,
|
||||||
|
for testing purposes only.
|
||||||
|
|
||||||
|
The root of the problem is that Python's long-integer arithmetic is
|
||||||
|
not designed for use in cryptography. Specifically, it may take more
|
||||||
|
or less time to execute an operation depending on the values of the
|
||||||
|
inputs, and its memory access patterns may also depend on the inputs.
|
||||||
|
This opens it to timing and cache side-channel attacks which can
|
||||||
|
disclose data to an attacker. We rely on Python's long-integer
|
||||||
|
arithmetic, so we cannot handle secrets without risking their disclosure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from typing import List, NewType, Tuple
|
||||||
|
|
||||||
|
Point = NewType("Point", Tuple[int, int, int, int])
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "1.0.dev1"
|
||||||
|
|
||||||
|
|
||||||
|
b = 256
|
||||||
|
q = 2 ** 255 - 19
|
||||||
|
l = 2 ** 252 + 27742317777372353535851937790883648493
|
||||||
|
|
||||||
|
COORD_MASK = ~(1 + 2 + 4 + (1 << b - 1))
|
||||||
|
COORD_HIGH_BIT = 1 << b - 2
|
||||||
|
|
||||||
|
|
||||||
|
def H(m: bytes) -> bytes:
|
||||||
|
return hashlib.sha512(m).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def pow2(x: int, p: int) -> int:
|
||||||
|
"""== pow(x, 2**p, q)"""
|
||||||
|
while p > 0:
|
||||||
|
x = x * x % q
|
||||||
|
p -= 1
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def inv(z: int) -> int:
|
||||||
|
"""$= z^{-1} mod q$, for z != 0"""
|
||||||
|
# Adapted from curve25519_athlon.c in djb's Curve25519.
|
||||||
|
z2 = z * z % q # 2
|
||||||
|
z9 = pow2(z2, 2) * z % q # 9
|
||||||
|
z11 = z9 * z2 % q # 11
|
||||||
|
z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0
|
||||||
|
z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0
|
||||||
|
z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ...
|
||||||
|
z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
|
||||||
|
z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
|
||||||
|
z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
|
||||||
|
z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
|
||||||
|
z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0
|
||||||
|
return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2
|
||||||
|
|
||||||
|
|
||||||
|
d = -121665 * inv(121666) % q
|
||||||
|
I = pow(2, (q - 1) // 4, q)
|
||||||
|
|
||||||
|
|
||||||
|
def xrecover(y: int) -> int:
|
||||||
|
xx = (y * y - 1) * inv(d * y * y + 1)
|
||||||
|
x = pow(xx, (q + 3) // 8, q)
|
||||||
|
|
||||||
|
if (x * x - xx) % q != 0:
|
||||||
|
x = (x * I) % q
|
||||||
|
|
||||||
|
if x % 2 != 0:
|
||||||
|
x = q - x
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
By = 4 * inv(5)
|
||||||
|
Bx = xrecover(By)
|
||||||
|
B = Point((Bx % q, By % q, 1, (Bx * By) % q))
|
||||||
|
ident = Point((0, 1, 1, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def edwards_add(P: Point, Q: Point) -> Point:
|
||||||
|
# This is formula sequence 'addition-add-2008-hwcd-3' from
|
||||||
|
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||||
|
(x1, y1, z1, t1) = P
|
||||||
|
(x2, y2, z2, t2) = Q
|
||||||
|
|
||||||
|
a = (y1 - x1) * (y2 - x2) % q
|
||||||
|
b = (y1 + x1) * (y2 + x2) % q
|
||||||
|
c = t1 * 2 * d * t2 % q
|
||||||
|
dd = z1 * 2 * z2 % q
|
||||||
|
e = b - a
|
||||||
|
f = dd - c
|
||||||
|
g = dd + c
|
||||||
|
h = b + a
|
||||||
|
x3 = e * f
|
||||||
|
y3 = g * h
|
||||||
|
t3 = e * h
|
||||||
|
z3 = f * g
|
||||||
|
|
||||||
|
return Point((x3 % q, y3 % q, z3 % q, t3 % q))
|
||||||
|
|
||||||
|
|
||||||
|
def edwards_double(P: Point) -> Point:
|
||||||
|
# This is formula sequence 'dbl-2008-hwcd' from
|
||||||
|
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||||
|
(x1, y1, z1, _) = P
|
||||||
|
|
||||||
|
a = x1 * x1 % q
|
||||||
|
b = y1 * y1 % q
|
||||||
|
c = 2 * z1 * z1 % q
|
||||||
|
# dd = -a
|
||||||
|
e = ((x1 + y1) * (x1 + y1) - a - b) % q
|
||||||
|
g = -a + b # dd + b
|
||||||
|
f = g - c
|
||||||
|
h = -a - b # dd - b
|
||||||
|
x3 = e * f
|
||||||
|
y3 = g * h
|
||||||
|
t3 = e * h
|
||||||
|
z3 = f * g
|
||||||
|
|
||||||
|
return Point((x3 % q, y3 % q, z3 % q, t3 % q))
|
||||||
|
|
||||||
|
|
||||||
|
def scalarmult(P: Point, e: int) -> Point:
|
||||||
|
if e == 0:
|
||||||
|
return ident
|
||||||
|
Q = scalarmult(P, e // 2)
|
||||||
|
Q = edwards_double(Q)
|
||||||
|
if e & 1:
|
||||||
|
Q = edwards_add(Q, P)
|
||||||
|
return Q
|
||||||
|
|
||||||
|
|
||||||
|
# Bpow[i] == scalarmult(B, 2**i)
|
||||||
|
Bpow = [] # type: List[Point]
|
||||||
|
|
||||||
|
|
||||||
|
def make_Bpow() -> None:
|
||||||
|
P = B
|
||||||
|
for _ in range(253):
|
||||||
|
Bpow.append(P)
|
||||||
|
P = edwards_double(P)
|
||||||
|
|
||||||
|
|
||||||
|
make_Bpow()
|
||||||
|
|
||||||
|
|
||||||
|
def scalarmult_B(e: int) -> Point:
|
||||||
|
"""
|
||||||
|
Implements scalarmult(B, e) more efficiently.
|
||||||
|
"""
|
||||||
|
# scalarmult(B, l) is the identity
|
||||||
|
e = e % l
|
||||||
|
P = ident
|
||||||
|
for i in range(253):
|
||||||
|
if e & 1:
|
||||||
|
P = edwards_add(P, Bpow[i])
|
||||||
|
e = e // 2
|
||||||
|
assert e == 0, e
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def encodeint(y: int) -> bytes:
|
||||||
|
return y.to_bytes(b // 8, "little")
|
||||||
|
|
||||||
|
|
||||||
|
def encodepoint(P: Point) -> bytes:
|
||||||
|
(x, y, z, _) = P
|
||||||
|
zi = inv(z)
|
||||||
|
x = (x * zi) % q
|
||||||
|
y = (y * zi) % q
|
||||||
|
|
||||||
|
xbit = (x & 1) << (b - 1)
|
||||||
|
y_result = y & ~xbit # clear x bit
|
||||||
|
y_result |= xbit # set corret x bit value
|
||||||
|
return encodeint(y_result)
|
||||||
|
|
||||||
|
|
||||||
|
def decodeint(s: bytes) -> int:
|
||||||
|
return int.from_bytes(s, "little")
|
||||||
|
|
||||||
|
|
||||||
|
def decodepoint(s: bytes) -> Point:
|
||||||
|
y = decodeint(s) & ~(1 << b - 1) # y without the highest bit
|
||||||
|
x = xrecover(y)
|
||||||
|
if x & 1 != bit(s, b - 1):
|
||||||
|
x = q - x
|
||||||
|
P = Point((x, y, 1, (x * y) % q))
|
||||||
|
if not isoncurve(P):
|
||||||
|
raise ValueError("decoding point that is not on curve")
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def decodecoord(s: bytes) -> int:
|
||||||
|
a = decodeint(s[: b // 8])
|
||||||
|
# clear mask bits
|
||||||
|
a &= COORD_MASK
|
||||||
|
# set high bit
|
||||||
|
a |= COORD_HIGH_BIT
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def bit(h: bytes, i: int) -> int:
|
||||||
|
return (h[i // 8] >> (i % 8)) & 1
|
||||||
|
|
||||||
|
|
||||||
|
def publickey_unsafe(sk: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Not safe to use with secret keys or secret data.
|
||||||
|
|
||||||
|
See module docstring. This function should be used for testing only.
|
||||||
|
"""
|
||||||
|
h = H(sk)
|
||||||
|
a = decodecoord(h)
|
||||||
|
A = scalarmult_B(a)
|
||||||
|
return encodepoint(A)
|
||||||
|
|
||||||
|
|
||||||
|
def Hint(m: bytes) -> int:
|
||||||
|
return decodeint(H(m))
|
||||||
|
|
||||||
|
|
||||||
|
def signature_unsafe(m: bytes, sk: bytes, pk: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Not safe to use with secret keys or secret data.
|
||||||
|
|
||||||
|
See module docstring. This function should be used for testing only.
|
||||||
|
"""
|
||||||
|
h = H(sk)
|
||||||
|
a = decodecoord(h)
|
||||||
|
r = Hint(h[b // 8 : b // 4] + m)
|
||||||
|
R = scalarmult_B(r)
|
||||||
|
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
|
||||||
|
return encodepoint(R) + encodeint(S)
|
||||||
|
|
||||||
|
|
||||||
|
def isoncurve(P: Point) -> bool:
|
||||||
|
(x, y, z, t) = P
|
||||||
|
return (
|
||||||
|
z % q != 0
|
||||||
|
and x * y % q == z * t % q
|
||||||
|
and (y * y - x * x - z * z - d * t * t) % q == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureMismatch(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def checkvalid(s: bytes, m: bytes, pk: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Not safe to use when any argument is secret.
|
||||||
|
|
||||||
|
See module docstring. This function should be used only for
|
||||||
|
verifying public signatures of public messages.
|
||||||
|
"""
|
||||||
|
if len(s) != b // 4:
|
||||||
|
raise ValueError("signature length is wrong")
|
||||||
|
|
||||||
|
if len(pk) != b // 8:
|
||||||
|
raise ValueError("public-key length is wrong")
|
||||||
|
|
||||||
|
R = decodepoint(s[: b // 8])
|
||||||
|
A = decodepoint(pk)
|
||||||
|
S = decodeint(s[b // 8 : b // 4])
|
||||||
|
h = Hint(encodepoint(R) + pk + m)
|
||||||
|
|
||||||
|
(x1, y1, z1, _) = P = scalarmult_B(S)
|
||||||
|
(x2, y2, z2, _) = Q = edwards_add(R, scalarmult(A, h))
|
||||||
|
|
||||||
|
if (
|
||||||
|
not isoncurve(P)
|
||||||
|
or not isoncurve(Q)
|
||||||
|
or (x1 * z2 - x2 * z1) % q != 0
|
||||||
|
or (y1 * z2 - y2 * z1) % q != 0
|
||||||
|
):
|
||||||
|
raise SignatureMismatch("signature does not pass verification")
|
193
python/trezorlib/btc.py
Normal file
193
python/trezorlib/btc.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import coins, messages
|
||||||
|
from .tools import CallException, expect, normalize_nfc, session
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.PublicKey)
|
||||||
|
def get_public_node(
|
||||||
|
client,
|
||||||
|
n,
|
||||||
|
ecdsa_curve_name=None,
|
||||||
|
show_display=False,
|
||||||
|
coin_name=None,
|
||||||
|
script_type=messages.InputScriptType.SPENDADDRESS,
|
||||||
|
):
|
||||||
|
return client.call(
|
||||||
|
messages.GetPublicKey(
|
||||||
|
address_n=n,
|
||||||
|
ecdsa_curve_name=ecdsa_curve_name,
|
||||||
|
show_display=show_display,
|
||||||
|
coin_name=coin_name,
|
||||||
|
script_type=script_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.Address, field="address")
|
||||||
|
def get_address(
|
||||||
|
client,
|
||||||
|
coin_name,
|
||||||
|
n,
|
||||||
|
show_display=False,
|
||||||
|
multisig=None,
|
||||||
|
script_type=messages.InputScriptType.SPENDADDRESS,
|
||||||
|
):
|
||||||
|
return client.call(
|
||||||
|
messages.GetAddress(
|
||||||
|
address_n=n,
|
||||||
|
coin_name=coin_name,
|
||||||
|
show_display=show_display,
|
||||||
|
multisig=multisig,
|
||||||
|
script_type=script_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.MessageSignature)
|
||||||
|
def sign_message(
|
||||||
|
client, coin_name, n, message, script_type=messages.InputScriptType.SPENDADDRESS
|
||||||
|
):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
return client.call(
|
||||||
|
messages.SignMessage(
|
||||||
|
coin_name=coin_name, address_n=n, message=message, script_type=script_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_message(client, coin_name, address, signature, message):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
try:
|
||||||
|
resp = client.call(
|
||||||
|
messages.VerifyMessage(
|
||||||
|
address=address,
|
||||||
|
signature=signature,
|
||||||
|
message=message,
|
||||||
|
coin_name=coin_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except CallException as e:
|
||||||
|
resp = e
|
||||||
|
return isinstance(resp, messages.Success)
|
||||||
|
|
||||||
|
|
||||||
|
@session
|
||||||
|
def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None):
|
||||||
|
# set up a transactions dict
|
||||||
|
txes = {None: messages.TransactionType(inputs=inputs, outputs=outputs)}
|
||||||
|
# preload all relevant transactions ahead of time
|
||||||
|
if coin_name in coins.by_name:
|
||||||
|
load_prevtxes = not coins.by_name[coin_name]["force_bip143"]
|
||||||
|
else:
|
||||||
|
load_prevtxes = True
|
||||||
|
if load_prevtxes:
|
||||||
|
for inp in inputs:
|
||||||
|
if inp.script_type not in (
|
||||||
|
messages.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
messages.InputScriptType.SPENDWITNESS,
|
||||||
|
messages.InputScriptType.EXTERNAL,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
prev_tx = prev_txes[inp.prev_hash]
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError("Could not retrieve prev_tx") from e
|
||||||
|
if not isinstance(prev_tx, messages.TransactionType):
|
||||||
|
raise ValueError("Invalid value for prev_tx") from None
|
||||||
|
txes[inp.prev_hash] = prev_tx
|
||||||
|
|
||||||
|
if details is None:
|
||||||
|
signtx = messages.SignTx()
|
||||||
|
else:
|
||||||
|
signtx = details
|
||||||
|
|
||||||
|
signtx.coin_name = coin_name
|
||||||
|
signtx.inputs_count = len(inputs)
|
||||||
|
signtx.outputs_count = len(outputs)
|
||||||
|
|
||||||
|
res = client.call(signtx)
|
||||||
|
|
||||||
|
# Prepare structure for signatures
|
||||||
|
signatures = [None] * len(inputs)
|
||||||
|
serialized_tx = b""
|
||||||
|
|
||||||
|
def copy_tx_meta(tx):
|
||||||
|
tx_copy = messages.TransactionType(**tx)
|
||||||
|
# clear fields
|
||||||
|
tx_copy.inputs_cnt = len(tx.inputs)
|
||||||
|
tx_copy.inputs = []
|
||||||
|
tx_copy.outputs_cnt = len(tx.bin_outputs or tx.outputs)
|
||||||
|
tx_copy.outputs = []
|
||||||
|
tx_copy.bin_outputs = []
|
||||||
|
tx_copy.extra_data_len = len(tx.extra_data or b"")
|
||||||
|
tx_copy.extra_data = None
|
||||||
|
return tx_copy
|
||||||
|
|
||||||
|
R = messages.RequestType
|
||||||
|
while isinstance(res, messages.TxRequest):
|
||||||
|
# If there's some part of signed transaction, let's add it
|
||||||
|
if res.serialized:
|
||||||
|
if res.serialized.serialized_tx:
|
||||||
|
serialized_tx += res.serialized.serialized_tx
|
||||||
|
|
||||||
|
if res.serialized.signature_index is not None:
|
||||||
|
idx = res.serialized.signature_index
|
||||||
|
sig = res.serialized.signature
|
||||||
|
if signatures[idx] is not None:
|
||||||
|
raise ValueError("Signature for index %d already filled" % idx)
|
||||||
|
signatures[idx] = sig
|
||||||
|
|
||||||
|
if res.request_type == R.TXFINISHED:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Device asked for one more information, let's process it.
|
||||||
|
current_tx = txes[res.details.tx_hash]
|
||||||
|
|
||||||
|
if res.request_type == R.TXMETA:
|
||||||
|
msg = copy_tx_meta(current_tx)
|
||||||
|
res = client.call(messages.TxAck(tx=msg))
|
||||||
|
|
||||||
|
elif res.request_type == R.TXINPUT:
|
||||||
|
msg = messages.TransactionType()
|
||||||
|
msg.inputs = [current_tx.inputs[res.details.request_index]]
|
||||||
|
res = client.call(messages.TxAck(tx=msg))
|
||||||
|
|
||||||
|
elif res.request_type == R.TXOUTPUT:
|
||||||
|
msg = messages.TransactionType()
|
||||||
|
if res.details.tx_hash:
|
||||||
|
msg.bin_outputs = [current_tx.bin_outputs[res.details.request_index]]
|
||||||
|
else:
|
||||||
|
msg.outputs = [current_tx.outputs[res.details.request_index]]
|
||||||
|
|
||||||
|
res = client.call(messages.TxAck(tx=msg))
|
||||||
|
|
||||||
|
elif res.request_type == R.TXEXTRADATA:
|
||||||
|
o, l = res.details.extra_data_offset, res.details.extra_data_len
|
||||||
|
msg = messages.TransactionType()
|
||||||
|
msg.extra_data = current_tx.extra_data[o : o + l]
|
||||||
|
res = client.call(messages.TxAck(tx=msg))
|
||||||
|
|
||||||
|
if isinstance(res, messages.Failure):
|
||||||
|
raise CallException("Signing failed")
|
||||||
|
|
||||||
|
if not isinstance(res, messages.TxRequest):
|
||||||
|
raise CallException("Unexpected message")
|
||||||
|
|
||||||
|
if None in signatures:
|
||||||
|
raise RuntimeError("Some signatures are missing!")
|
||||||
|
|
||||||
|
return signatures, serialized_tx
|
92
python/trezorlib/cardano.py
Normal file
92
python/trezorlib/cardano.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from . import messages, tools
|
||||||
|
from .tools import expect, session
|
||||||
|
|
||||||
|
REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs", "transactions")
|
||||||
|
REQUIRED_FIELDS_INPUT = ("path", "prev_hash", "prev_index", "type")
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.CardanoAddress, field="address")
|
||||||
|
def get_address(client, address_n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
messages.CardanoGetAddress(address_n=address_n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.CardanoPublicKey)
|
||||||
|
def get_public_key(client, address_n):
|
||||||
|
return client.call(messages.CardanoGetPublicKey(address_n=address_n))
|
||||||
|
|
||||||
|
|
||||||
|
@session
|
||||||
|
def sign_tx(
|
||||||
|
client,
|
||||||
|
inputs: List[messages.CardanoTxInputType],
|
||||||
|
outputs: List[messages.CardanoTxOutputType],
|
||||||
|
transactions: List[bytes],
|
||||||
|
protocol_magic,
|
||||||
|
):
|
||||||
|
response = client.call(
|
||||||
|
messages.CardanoSignTx(
|
||||||
|
inputs=inputs,
|
||||||
|
outputs=outputs,
|
||||||
|
transactions_count=len(transactions),
|
||||||
|
protocol_magic=protocol_magic,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while isinstance(response, messages.CardanoTxRequest):
|
||||||
|
tx_index = response.tx_index
|
||||||
|
|
||||||
|
transaction_data = bytes.fromhex(transactions[tx_index])
|
||||||
|
ack_message = messages.CardanoTxAck(transaction=transaction_data)
|
||||||
|
response = client.call(ack_message)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def create_input(input) -> messages.CardanoTxInputType:
|
||||||
|
if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT):
|
||||||
|
raise ValueError("The input is missing some fields")
|
||||||
|
|
||||||
|
path = input["path"]
|
||||||
|
|
||||||
|
return messages.CardanoTxInputType(
|
||||||
|
address_n=tools.parse_path(path),
|
||||||
|
prev_hash=bytes.fromhex(input["prev_hash"]),
|
||||||
|
prev_index=input["prev_index"],
|
||||||
|
type=input["type"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_output(output) -> messages.CardanoTxOutputType:
|
||||||
|
if not output.get("amount") or not (output.get("address") or output.get("path")):
|
||||||
|
raise ValueError("The output is missing some fields")
|
||||||
|
|
||||||
|
if output.get("path"):
|
||||||
|
path = output["path"]
|
||||||
|
|
||||||
|
return messages.CardanoTxOutputType(
|
||||||
|
address_n=tools.parse_path(path), amount=int(output["amount"])
|
||||||
|
)
|
||||||
|
|
||||||
|
return messages.CardanoTxOutputType(
|
||||||
|
address=output["address"], amount=int(output["amount"])
|
||||||
|
)
|
21
python/trezorlib/ckd_public.py
Normal file
21
python/trezorlib/ckd_public.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .tests.support.ckd_public import * # noqa
|
||||||
|
|
||||||
|
warnings.warn("ckd_public module is deprecated and will be removed", DeprecationWarning)
|
379
python/trezorlib/client.py
Normal file
379
python/trezorlib/client.py
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
from . import MINIMUM_FIRMWARE_VERSION, exceptions, messages, tools
|
||||||
|
|
||||||
|
if sys.version_info.major < 3:
|
||||||
|
raise Exception("Trezorlib does not support Python 2 anymore.")
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VENDORS = ("bitcointrezor.com", "trezor.io")
|
||||||
|
MAX_PASSPHRASE_LENGTH = 50
|
||||||
|
|
||||||
|
DEPRECATION_ERROR = """
|
||||||
|
Incompatible Trezor library detected.
|
||||||
|
|
||||||
|
(Original error: {})
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
OUTDATED_FIRMWARE_ERROR = """
|
||||||
|
Your Trezor firmware is out of date. Update it with the following command:
|
||||||
|
trezorctl firmware-update
|
||||||
|
Or visit https://wallet.trezor.io/
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_buttonrequest_value(code):
|
||||||
|
# Converts integer code to its string representation of ButtonRequestType
|
||||||
|
return [
|
||||||
|
k
|
||||||
|
for k in dir(messages.ButtonRequestType)
|
||||||
|
if getattr(messages.ButtonRequestType, k) == code
|
||||||
|
][0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_client(path=None, ui=None, **kwargs):
|
||||||
|
"""Get a client for a connected Trezor device.
|
||||||
|
|
||||||
|
Returns a TrezorClient instance with minimum fuss.
|
||||||
|
|
||||||
|
If no path is specified, finds first connected Trezor. Otherwise performs
|
||||||
|
a prefix-search for the specified device. If no UI is supplied, instantiates
|
||||||
|
the default CLI UI.
|
||||||
|
"""
|
||||||
|
from .transport import get_transport
|
||||||
|
from .ui import ClickUI
|
||||||
|
|
||||||
|
transport = get_transport(path, prefix_search=True)
|
||||||
|
if ui is None:
|
||||||
|
ui = ClickUI()
|
||||||
|
|
||||||
|
return TrezorClient(transport, ui, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorClient:
|
||||||
|
"""Trezor client, a connection to a Trezor device.
|
||||||
|
|
||||||
|
This class allows you to manage connection state, send and receive protobuf
|
||||||
|
messages, handle user interactions, and perform some generic tasks
|
||||||
|
(send a cancel message, initialize or clear a session, ping the device).
|
||||||
|
|
||||||
|
You have to provide a transport, i.e., a raw connection to the device. You can use
|
||||||
|
`trezorlib.transport.get_transport` to find one.
|
||||||
|
|
||||||
|
You have to provide an UI implementation for the three kinds of interaction:
|
||||||
|
- button request (notify the user that their interaction is needed)
|
||||||
|
- PIN request (on T1, ask the user to input numbers for a PIN matrix)
|
||||||
|
- passphrase request (ask the user to enter a passphrase)
|
||||||
|
See `trezorlib.ui` for details.
|
||||||
|
|
||||||
|
You can supply a `state` you saved in the previous session. If you do,
|
||||||
|
the user might not need to enter their passphrase again.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, transport, ui=None, state=None):
|
||||||
|
LOG.info("creating client instance for device: {}".format(transport.get_path()))
|
||||||
|
self.transport = transport
|
||||||
|
self.ui = ui
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
if ui is None:
|
||||||
|
warnings.warn("UI class not supplied. This will probably crash soon.")
|
||||||
|
|
||||||
|
self.session_counter = 0
|
||||||
|
self.init_device()
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
if self.session_counter == 0:
|
||||||
|
self.transport.begin_session()
|
||||||
|
self.session_counter += 1
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.session_counter == 1:
|
||||||
|
self.transport.end_session()
|
||||||
|
self.session_counter -= 1
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self._raw_write(messages.Cancel())
|
||||||
|
|
||||||
|
def call_raw(self, msg):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
self._raw_write(msg)
|
||||||
|
return self._raw_read()
|
||||||
|
|
||||||
|
def _raw_write(self, msg):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
self.transport.write(msg)
|
||||||
|
|
||||||
|
def _raw_read(self):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
return self.transport.read()
|
||||||
|
|
||||||
|
def _callback_pin(self, msg):
|
||||||
|
try:
|
||||||
|
pin = self.ui.get_pin(msg.type)
|
||||||
|
except exceptions.Cancelled:
|
||||||
|
self.call_raw(messages.Cancel())
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not pin.isdigit():
|
||||||
|
self.call_raw(messages.Cancel())
|
||||||
|
raise ValueError("Non-numeric PIN provided")
|
||||||
|
|
||||||
|
resp = self.call_raw(messages.PinMatrixAck(pin=pin))
|
||||||
|
if isinstance(resp, messages.Failure) and resp.code in (
|
||||||
|
messages.FailureType.PinInvalid,
|
||||||
|
messages.FailureType.PinCancelled,
|
||||||
|
messages.FailureType.PinExpected,
|
||||||
|
):
|
||||||
|
raise exceptions.PinException(resp.code, resp.message)
|
||||||
|
else:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def _callback_passphrase(self, msg):
|
||||||
|
if msg.on_device:
|
||||||
|
passphrase = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
passphrase = self.ui.get_passphrase()
|
||||||
|
except exceptions.Cancelled:
|
||||||
|
self.call_raw(messages.Cancel())
|
||||||
|
raise
|
||||||
|
|
||||||
|
passphrase = Mnemonic.normalize_string(passphrase)
|
||||||
|
if len(passphrase) > MAX_PASSPHRASE_LENGTH:
|
||||||
|
self.call_raw(messages.Cancel())
|
||||||
|
raise ValueError("Passphrase too long")
|
||||||
|
|
||||||
|
resp = self.call_raw(
|
||||||
|
messages.PassphraseAck(passphrase=passphrase, state=self.state)
|
||||||
|
)
|
||||||
|
if isinstance(resp, messages.PassphraseStateRequest):
|
||||||
|
# TODO report to the user that the passphrase has changed?
|
||||||
|
self.state = resp.state
|
||||||
|
return self.call_raw(messages.PassphraseStateAck())
|
||||||
|
else:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def _callback_button(self, msg):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
# do this raw - send ButtonAck first, notify UI later
|
||||||
|
self._raw_write(messages.ButtonAck())
|
||||||
|
self.ui.button_request(msg.code)
|
||||||
|
return self._raw_read()
|
||||||
|
|
||||||
|
@tools.session
|
||||||
|
def call(self, msg):
|
||||||
|
self.check_firmware_version()
|
||||||
|
resp = self.call_raw(msg)
|
||||||
|
while True:
|
||||||
|
if isinstance(resp, messages.PinMatrixRequest):
|
||||||
|
resp = self._callback_pin(resp)
|
||||||
|
elif isinstance(resp, messages.PassphraseRequest):
|
||||||
|
resp = self._callback_passphrase(resp)
|
||||||
|
elif isinstance(resp, messages.ButtonRequest):
|
||||||
|
resp = self._callback_button(resp)
|
||||||
|
elif isinstance(resp, messages.Failure):
|
||||||
|
if resp.code == messages.FailureType.ActionCancelled:
|
||||||
|
raise exceptions.Cancelled
|
||||||
|
raise exceptions.TrezorFailure(resp)
|
||||||
|
else:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@tools.session
|
||||||
|
def init_device(self):
|
||||||
|
resp = self.call_raw(messages.Initialize(state=self.state))
|
||||||
|
if not isinstance(resp, messages.Features):
|
||||||
|
raise exceptions.TrezorException("Unexpected initial response")
|
||||||
|
else:
|
||||||
|
self.features = resp
|
||||||
|
if self.features.vendor not in VENDORS:
|
||||||
|
raise RuntimeError("Unsupported device")
|
||||||
|
# A side-effect of this is a sanity check for broken protobuf definitions.
|
||||||
|
# If the `vendor` field doesn't exist, you probably have a mismatched
|
||||||
|
# checkout of trezor-common.
|
||||||
|
self.version = (
|
||||||
|
self.features.major_version,
|
||||||
|
self.features.minor_version,
|
||||||
|
self.features.patch_version,
|
||||||
|
)
|
||||||
|
self.check_firmware_version(warn_only=True)
|
||||||
|
|
||||||
|
def is_outdated(self):
|
||||||
|
if self.features.bootloader_mode:
|
||||||
|
return False
|
||||||
|
model = self.features.model or "1"
|
||||||
|
required_version = MINIMUM_FIRMWARE_VERSION[model]
|
||||||
|
return self.version < required_version
|
||||||
|
|
||||||
|
def check_firmware_version(self, warn_only=False):
|
||||||
|
if self.is_outdated():
|
||||||
|
if warn_only:
|
||||||
|
warnings.warn(OUTDATED_FIRMWARE_ERROR, stacklevel=2)
|
||||||
|
else:
|
||||||
|
raise exceptions.OutdatedFirmwareError(OUTDATED_FIRMWARE_ERROR)
|
||||||
|
|
||||||
|
@tools.expect(messages.Success, field="message")
|
||||||
|
def ping(
|
||||||
|
self,
|
||||||
|
msg,
|
||||||
|
button_protection=False,
|
||||||
|
pin_protection=False,
|
||||||
|
passphrase_protection=False,
|
||||||
|
):
|
||||||
|
# We would like ping to work on any valid TrezorClient instance, but
|
||||||
|
# due to the protection modes, we need to go through self.call, and that will
|
||||||
|
# raise an exception if the firmware is too old.
|
||||||
|
# So we short-circuit the simplest variant of ping with call_raw.
|
||||||
|
if not button_protection and not pin_protection and not passphrase_protection:
|
||||||
|
# XXX this should be: `with self:`
|
||||||
|
try:
|
||||||
|
self.open()
|
||||||
|
return self.call_raw(messages.Ping(message=msg))
|
||||||
|
finally:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
msg = messages.Ping(
|
||||||
|
message=msg,
|
||||||
|
button_protection=button_protection,
|
||||||
|
pin_protection=pin_protection,
|
||||||
|
passphrase_protection=passphrase_protection,
|
||||||
|
)
|
||||||
|
return self.call(msg)
|
||||||
|
|
||||||
|
def get_device_id(self):
|
||||||
|
return self.features.device_id
|
||||||
|
|
||||||
|
@tools.expect(messages.Success, field="message")
|
||||||
|
@tools.session
|
||||||
|
def clear_session(self):
|
||||||
|
return self.call_raw(messages.ClearSession())
|
||||||
|
|
||||||
|
|
||||||
|
def MovedTo(where):
|
||||||
|
def moved_to(*args, **kwargs):
|
||||||
|
msg = "Function has been moved to " + where
|
||||||
|
raise RuntimeError(DEPRECATION_ERROR.format(msg))
|
||||||
|
|
||||||
|
return moved_to
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolMixin(object):
|
||||||
|
"""Fake mixin for old-style software that constructed TrezorClient class
|
||||||
|
from separate mixins.
|
||||||
|
|
||||||
|
Now it only simulates existence of original attributes to prevent some early
|
||||||
|
crashes, and raises errors when any of the attributes are actually called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn("TrezorClient mixins are not supported anymore")
|
||||||
|
self.tx_api = None # Electrum checks that this attribute exists
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def set_tx_api(self, tx_api):
|
||||||
|
warnings.warn("set_tx_api is deprecated, use new arguments to sign_tx")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def expand_path(n):
|
||||||
|
warnings.warn(
|
||||||
|
"expand_path is deprecated, use tools.parse_path",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return tools.parse_path(n)
|
||||||
|
|
||||||
|
# Device functionality
|
||||||
|
wipe_device = MovedTo("device.wipe")
|
||||||
|
recovery_device = MovedTo("device.recover")
|
||||||
|
reset_device = MovedTo("device.reset")
|
||||||
|
backup_device = MovedTo("device.backup")
|
||||||
|
|
||||||
|
set_u2f_counter = MovedTo("device.set_u2f_counter")
|
||||||
|
|
||||||
|
apply_settings = MovedTo("device.apply_settings")
|
||||||
|
apply_flags = MovedTo("device.apply_flags")
|
||||||
|
change_pin = MovedTo("device.change_pin")
|
||||||
|
|
||||||
|
# Firmware functionality
|
||||||
|
firmware_update = MovedTo("firmware.update")
|
||||||
|
|
||||||
|
# BTC-like functionality
|
||||||
|
get_public_node = MovedTo("btc.get_public_node")
|
||||||
|
get_address = MovedTo("btc.get_address")
|
||||||
|
sign_tx = MovedTo("btc.sign_tx")
|
||||||
|
sign_message = MovedTo("btc.sign_message")
|
||||||
|
verify_message = MovedTo("btc.verify_message")
|
||||||
|
|
||||||
|
# CoSi functionality
|
||||||
|
cosi_commit = MovedTo("cosi.commit")
|
||||||
|
cosi_sign = MovedTo("cosi.sign")
|
||||||
|
|
||||||
|
# Ethereum functionality
|
||||||
|
ethereum_get_address = MovedTo("ethereum.get_address")
|
||||||
|
ethereum_sign_tx = MovedTo("ethereum.sign_tx")
|
||||||
|
ethereum_sign_message = MovedTo("ethereum.sign_message")
|
||||||
|
ethereum_verify_message = MovedTo("ethereum.verify_message")
|
||||||
|
|
||||||
|
# Lisk functionality
|
||||||
|
lisk_get_address = MovedTo("lisk.get_address")
|
||||||
|
lisk_get_public_key = MovedTo("lisk.get_public_key")
|
||||||
|
lisk_sign_message = MovedTo("lisk.sign_message")
|
||||||
|
lisk_verify_message = MovedTo("lisk.verify_message")
|
||||||
|
lisk_sign_tx = MovedTo("lisk.sign_tx")
|
||||||
|
|
||||||
|
# NEM functionality
|
||||||
|
nem_get_address = MovedTo("nem.get_address")
|
||||||
|
nem_sign_tx = MovedTo("nem.sign_tx")
|
||||||
|
|
||||||
|
# Stellar functionality
|
||||||
|
stellar_get_address = MovedTo("stellar.get_address")
|
||||||
|
stellar_sign_transaction = MovedTo("stellar.sign_tx")
|
||||||
|
|
||||||
|
# Miscellaneous cryptographic functionality
|
||||||
|
get_entropy = MovedTo("misc.get_entropy")
|
||||||
|
sign_identity = MovedTo("misc.sign_identity")
|
||||||
|
get_ecdh_session_key = MovedTo("misc.get_ecdh_session_key")
|
||||||
|
encrypt_keyvalue = MovedTo("misc.encrypt_keyvalue")
|
||||||
|
decrypt_keyvalue = MovedTo("misc.decrypt_keyvalue")
|
||||||
|
|
||||||
|
# Debug device functionality
|
||||||
|
load_device_by_mnemonic = MovedTo("debuglink.load_device_by_mnemonic")
|
||||||
|
load_device_by_xprv = MovedTo("debuglink.load_device_by_xprv")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseClient:
|
||||||
|
"""Compatibility proxy for original BaseClient class.
|
||||||
|
Prevents early crash in Electrum forks and possibly other software.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn("TrezorClient mixins are not supported anymore")
|
||||||
|
self.trezor_client = TrezorClient(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
return getattr(self.trezor_client, key)
|
||||||
|
|
||||||
|
|
||||||
|
# further Electrum compatibility
|
||||||
|
proto = None
|
51
python/trezorlib/coins.py
Normal file
51
python/trezorlib/coins.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from .tx_api import TxApi
|
||||||
|
|
||||||
|
COINS_JSON = os.path.join(os.path.dirname(__file__), "coins.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_coins_json():
|
||||||
|
# Load coins.json to local variables
|
||||||
|
# NOTE: coins.json comes from 'vendor/trezor-common/coins.json',
|
||||||
|
# which is a git submodule. If you're trying to run trezorlib directly
|
||||||
|
# from the checkout (or tarball), initialize the submodule with:
|
||||||
|
# $ git submodule update --init
|
||||||
|
# and install coins.json with:
|
||||||
|
# $ python setup.py prebuild
|
||||||
|
with open(COINS_JSON) as coins_json:
|
||||||
|
return json.load(coins_json)
|
||||||
|
|
||||||
|
|
||||||
|
# exported variables
|
||||||
|
__all__ = ["by_name", "slip44", "tx_api"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
coins_list = _load_coins_json()
|
||||||
|
by_name = {coin["coin_name"]: coin for coin in coins_list}
|
||||||
|
except Exception as e:
|
||||||
|
raise ImportError("Failed to load coins.json. Check your installation.") from e
|
||||||
|
|
||||||
|
slip44 = {name: coin["slip44"] for name, coin in by_name.items()}
|
||||||
|
tx_api = {
|
||||||
|
name: TxApi(coin)
|
||||||
|
for name, coin in by_name.items()
|
||||||
|
if coin["blockbook"] or coin["bitcore"]
|
||||||
|
}
|
138
python/trezorlib/cosi.py
Normal file
138
python/trezorlib/cosi.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from functools import reduce
|
||||||
|
from typing import Iterable, List, Tuple
|
||||||
|
|
||||||
|
from . import _ed25519, messages
|
||||||
|
from .tools import expect
|
||||||
|
|
||||||
|
# XXX, these could be NewType's, but that would infect users of the cosi module with these types as well.
|
||||||
|
# Unsure if we want that.
|
||||||
|
Ed25519PrivateKey = bytes
|
||||||
|
Ed25519PublicPoint = bytes
|
||||||
|
Ed25519Signature = bytes
|
||||||
|
|
||||||
|
|
||||||
|
def combine_keys(pks: Iterable[Ed25519PublicPoint]) -> Ed25519PublicPoint:
|
||||||
|
"""Combine a list of Ed25519 points into a "global" CoSi key."""
|
||||||
|
P = [_ed25519.decodepoint(pk) for pk in pks]
|
||||||
|
combine = reduce(_ed25519.edwards_add, P)
|
||||||
|
return Ed25519PublicPoint(_ed25519.encodepoint(combine))
|
||||||
|
|
||||||
|
|
||||||
|
def combine_sig(
|
||||||
|
global_R: Ed25519PublicPoint, sigs: Iterable[Ed25519Signature]
|
||||||
|
) -> Ed25519Signature:
|
||||||
|
"""Combine a list of signatures into a single CoSi signature."""
|
||||||
|
S = [_ed25519.decodeint(si) for si in sigs]
|
||||||
|
s = sum(S) % _ed25519.l
|
||||||
|
sig = global_R + _ed25519.encodeint(s)
|
||||||
|
return Ed25519Signature(sig)
|
||||||
|
|
||||||
|
|
||||||
|
def get_nonce(
|
||||||
|
sk: Ed25519PrivateKey, data: bytes, ctr: int = 0
|
||||||
|
) -> Tuple[int, Ed25519PublicPoint]:
|
||||||
|
"""Calculate CoSi nonces for given data.
|
||||||
|
These differ from Ed25519 deterministic nonces in that there is a counter appended at end.
|
||||||
|
|
||||||
|
Returns both the private point `r` and the partial signature `R`.
|
||||||
|
`r` is returned for performance reasons: :func:`sign_with_privkey`
|
||||||
|
takes it as its `nonce` argument so that it doesn't repeat the `get_nonce` call.
|
||||||
|
|
||||||
|
`R` should be combined with other partial signatures through :func:`combine_keys`
|
||||||
|
to obtain a "global commitment".
|
||||||
|
"""
|
||||||
|
# r = hash(hash(sk)[b .. 2b] + M + ctr)
|
||||||
|
# R = rB
|
||||||
|
h = _ed25519.H(sk)
|
||||||
|
bytesize = _ed25519.b // 8
|
||||||
|
assert len(h) == bytesize * 2
|
||||||
|
r = _ed25519.Hint(h[bytesize:] + data + ctr.to_bytes(4, "big"))
|
||||||
|
R = _ed25519.scalarmult(_ed25519.B, r)
|
||||||
|
return r, Ed25519PublicPoint(_ed25519.encodepoint(R))
|
||||||
|
|
||||||
|
|
||||||
|
def verify(
|
||||||
|
signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint
|
||||||
|
) -> None:
|
||||||
|
"""Verify Ed25519 signature. Raise exception if the signature is invalid."""
|
||||||
|
# XXX this *might* change to bool function
|
||||||
|
_ed25519.checkvalid(signature, digest, pub_key)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_m_of_n(
|
||||||
|
signature: Ed25519Signature,
|
||||||
|
digest: bytes,
|
||||||
|
m: int,
|
||||||
|
n: int,
|
||||||
|
mask: int,
|
||||||
|
keys: List[Ed25519PublicPoint],
|
||||||
|
) -> None:
|
||||||
|
if m < 1:
|
||||||
|
raise ValueError("At least 1 signer must be specified")
|
||||||
|
selected_keys = [keys[i] for i in range(n) if mask & (1 << i)]
|
||||||
|
if len(selected_keys) < m:
|
||||||
|
raise ValueError(
|
||||||
|
"Not enough signers ({} required, {} found)".format(m, len(selected_keys))
|
||||||
|
)
|
||||||
|
global_pk = combine_keys(selected_keys)
|
||||||
|
return verify(signature, digest, global_pk)
|
||||||
|
|
||||||
|
|
||||||
|
def pubkey_from_privkey(privkey: Ed25519PrivateKey) -> Ed25519PublicPoint:
|
||||||
|
"""Interpret 32 bytes of data as an Ed25519 private key.
|
||||||
|
Calculate and return the corresponding public key.
|
||||||
|
"""
|
||||||
|
return Ed25519PublicPoint(_ed25519.publickey_unsafe(privkey))
|
||||||
|
|
||||||
|
|
||||||
|
def sign_with_privkey(
|
||||||
|
digest: bytes,
|
||||||
|
privkey: Ed25519PrivateKey,
|
||||||
|
global_pubkey: Ed25519PublicPoint,
|
||||||
|
nonce: int,
|
||||||
|
global_commit: Ed25519PublicPoint,
|
||||||
|
) -> Ed25519Signature:
|
||||||
|
"""Create a CoSi signature of `digest` with the supplied private key.
|
||||||
|
This function needs to know the global public key and global commitment.
|
||||||
|
"""
|
||||||
|
h = _ed25519.H(privkey)
|
||||||
|
a = _ed25519.decodecoord(h)
|
||||||
|
|
||||||
|
S = (nonce + _ed25519.Hint(global_commit + global_pubkey + digest) * a) % _ed25519.l
|
||||||
|
return Ed25519Signature(_ed25519.encodeint(S))
|
||||||
|
|
||||||
|
|
||||||
|
# ====== Client functions ====== #
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.CosiCommitment)
|
||||||
|
def commit(client, n, data):
|
||||||
|
return client.call(messages.CosiCommit(address_n=n, data=data))
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.CosiSignature)
|
||||||
|
def sign(client, n, data, global_commitment, global_pubkey):
|
||||||
|
return client.call(
|
||||||
|
messages.CosiSign(
|
||||||
|
address_n=n,
|
||||||
|
data=data,
|
||||||
|
global_commitment=global_commitment,
|
||||||
|
global_pubkey=global_pubkey,
|
||||||
|
)
|
||||||
|
)
|
505
python/trezorlib/debuglink.py
Normal file
505
python/trezorlib/debuglink.py
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
from . import messages as proto, protobuf, tools
|
||||||
|
from .client import TrezorClient
|
||||||
|
from .tools import expect
|
||||||
|
|
||||||
|
EXPECTED_RESPONSES_CONTEXT_LINES = 3
|
||||||
|
|
||||||
|
|
||||||
|
class DebugLink:
|
||||||
|
def __init__(self, transport, auto_interact=True):
|
||||||
|
self.transport = transport
|
||||||
|
self.allow_interactions = auto_interact
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
self.transport.begin_session()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.transport.end_session()
|
||||||
|
|
||||||
|
def _call(self, msg, nowait=False):
|
||||||
|
self.transport.write(msg)
|
||||||
|
if nowait:
|
||||||
|
return None
|
||||||
|
ret = self.transport.read()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def state(self):
|
||||||
|
return self._call(proto.DebugLinkGetState())
|
||||||
|
|
||||||
|
def read_pin(self):
|
||||||
|
state = self.state()
|
||||||
|
return state.pin, state.matrix
|
||||||
|
|
||||||
|
def read_pin_encoded(self):
|
||||||
|
return self.encode_pin(*self.read_pin())
|
||||||
|
|
||||||
|
def encode_pin(self, pin, matrix=None):
|
||||||
|
"""Transform correct PIN according to the displayed matrix."""
|
||||||
|
if matrix is None:
|
||||||
|
_, matrix = self.read_pin()
|
||||||
|
return "".join([str(matrix.index(p) + 1) for p in pin])
|
||||||
|
|
||||||
|
def read_layout(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.layout
|
||||||
|
|
||||||
|
def read_mnemonic_secret(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.mnemonic_secret
|
||||||
|
|
||||||
|
def read_recovery_word(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return (obj.recovery_fake_word, obj.recovery_word_pos)
|
||||||
|
|
||||||
|
def read_reset_word(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.reset_word
|
||||||
|
|
||||||
|
def read_reset_word_pos(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.reset_word_pos
|
||||||
|
|
||||||
|
def read_reset_entropy(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.reset_entropy
|
||||||
|
|
||||||
|
def read_passphrase_protection(self):
|
||||||
|
obj = self._call(proto.DebugLinkGetState())
|
||||||
|
return obj.passphrase_protection
|
||||||
|
|
||||||
|
def input(self, word=None, button=None, swipe=None):
|
||||||
|
if not self.allow_interactions:
|
||||||
|
return
|
||||||
|
decision = proto.DebugLinkDecision()
|
||||||
|
if button is not None:
|
||||||
|
decision.yes_no = button
|
||||||
|
elif word is not None:
|
||||||
|
decision.input = word
|
||||||
|
elif swipe is not None:
|
||||||
|
decision.up_down = swipe
|
||||||
|
else:
|
||||||
|
raise ValueError("You need to provide input data.")
|
||||||
|
self._call(decision, nowait=True)
|
||||||
|
|
||||||
|
def press_button(self, yes_no):
|
||||||
|
self._call(proto.DebugLinkDecision(yes_no=yes_no), nowait=True)
|
||||||
|
|
||||||
|
def press_yes(self):
|
||||||
|
self.input(button=True)
|
||||||
|
|
||||||
|
def press_no(self):
|
||||||
|
self.input(button=False)
|
||||||
|
|
||||||
|
def swipe_up(self):
|
||||||
|
self.input(swipe=True)
|
||||||
|
|
||||||
|
def swipe_down(self):
|
||||||
|
self.input(swipe=False)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._call(proto.DebugLinkStop(), nowait=True)
|
||||||
|
|
||||||
|
@expect(proto.DebugLinkMemory, field="memory")
|
||||||
|
def memory_read(self, address, length):
|
||||||
|
return self._call(proto.DebugLinkMemoryRead(address=address, length=length))
|
||||||
|
|
||||||
|
def memory_write(self, address, memory, flash=False):
|
||||||
|
self._call(
|
||||||
|
proto.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash),
|
||||||
|
nowait=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def flash_erase(self, sector):
|
||||||
|
self._call(proto.DebugLinkFlashErase(sector=sector), nowait=True)
|
||||||
|
|
||||||
|
|
||||||
|
class NullDebugLink(DebugLink):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(None)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _call(self, msg, nowait=False):
|
||||||
|
if not nowait:
|
||||||
|
if isinstance(msg, proto.DebugLinkGetState):
|
||||||
|
return proto.DebugLinkState()
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected call to a fake debuglink")
|
||||||
|
|
||||||
|
|
||||||
|
class DebugUI:
|
||||||
|
INPUT_FLOW_DONE = object()
|
||||||
|
|
||||||
|
def __init__(self, debuglink: DebugLink):
|
||||||
|
self.debuglink = debuglink
|
||||||
|
self.pin = None
|
||||||
|
self.passphrase = "sphinx of black quartz, judge my wov"
|
||||||
|
self.input_flow = None
|
||||||
|
|
||||||
|
def button_request(self, code):
|
||||||
|
if self.input_flow is None:
|
||||||
|
self.debuglink.press_yes()
|
||||||
|
elif self.input_flow is self.INPUT_FLOW_DONE:
|
||||||
|
raise AssertionError("input flow ended prematurely")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.input_flow.send(code)
|
||||||
|
except StopIteration:
|
||||||
|
self.input_flow = self.INPUT_FLOW_DONE
|
||||||
|
|
||||||
|
def get_pin(self, code=None):
|
||||||
|
if self.pin:
|
||||||
|
return self.pin
|
||||||
|
else:
|
||||||
|
return self.debuglink.read_pin_encoded()
|
||||||
|
|
||||||
|
def get_passphrase(self):
|
||||||
|
return self.passphrase
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorClientDebugLink(TrezorClient):
|
||||||
|
# This class implements automatic responses
|
||||||
|
# and other functionality for unit tests
|
||||||
|
# for various callbacks, created in order
|
||||||
|
# to automatically pass unit tests.
|
||||||
|
#
|
||||||
|
# This mixing should be used only for purposes
|
||||||
|
# of unit testing, because it will fail to work
|
||||||
|
# without special DebugLink interface provided
|
||||||
|
# by the device.
|
||||||
|
|
||||||
|
def __init__(self, transport, auto_interact=True):
|
||||||
|
try:
|
||||||
|
debug_transport = transport.find_debug()
|
||||||
|
self.debug = DebugLink(debug_transport, auto_interact)
|
||||||
|
except Exception:
|
||||||
|
if not auto_interact:
|
||||||
|
self.debug = NullDebugLink()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.ui = DebugUI(self.debug)
|
||||||
|
|
||||||
|
self.in_with_statement = 0
|
||||||
|
self.screenshot_id = 0
|
||||||
|
|
||||||
|
self.filters = {}
|
||||||
|
|
||||||
|
# Always press Yes and provide correct pin
|
||||||
|
self.setup_debuglink(True, True)
|
||||||
|
|
||||||
|
# Do not expect any specific response from device
|
||||||
|
self.expected_responses = None
|
||||||
|
self.current_response = None
|
||||||
|
|
||||||
|
# Use blank passphrase
|
||||||
|
self.set_passphrase("")
|
||||||
|
super().__init__(transport, ui=self.ui)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
super().open()
|
||||||
|
self.debug.open()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.debug.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def set_filter(self, message_type, callback):
|
||||||
|
self.filters[message_type] = callback
|
||||||
|
|
||||||
|
def _filter_message(self, msg):
|
||||||
|
message_type = msg.__class__
|
||||||
|
callback = self.filters.get(message_type)
|
||||||
|
if callable(callback):
|
||||||
|
return callback(deepcopy(msg))
|
||||||
|
else:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def set_input_flow(self, input_flow):
|
||||||
|
if input_flow is None:
|
||||||
|
self.ui.input_flow = None
|
||||||
|
return
|
||||||
|
|
||||||
|
if callable(input_flow):
|
||||||
|
input_flow = input_flow()
|
||||||
|
if not hasattr(input_flow, "send"):
|
||||||
|
raise RuntimeError("input_flow should be a generator function")
|
||||||
|
self.ui.input_flow = input_flow
|
||||||
|
next(input_flow) # can't send before first yield
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
# For usage in with/expected_responses
|
||||||
|
self.in_with_statement += 1
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, traceback):
|
||||||
|
self.in_with_statement -= 1
|
||||||
|
|
||||||
|
if _type is not None:
|
||||||
|
# Another exception raised
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.expected_responses is None:
|
||||||
|
# no need to check anything else
|
||||||
|
return False
|
||||||
|
|
||||||
|
# return isinstance(value, TypeError)
|
||||||
|
# Evaluate missed responses in 'with' statement
|
||||||
|
if self.current_response < len(self.expected_responses):
|
||||||
|
self._raise_unexpected_response(None)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
self.expected_responses = None
|
||||||
|
self.current_response = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_expected_responses(self, expected):
|
||||||
|
if not self.in_with_statement:
|
||||||
|
raise RuntimeError("Must be called inside 'with' statement")
|
||||||
|
self.expected_responses = expected
|
||||||
|
self.current_response = 0
|
||||||
|
|
||||||
|
def setup_debuglink(self, button, pin_correct):
|
||||||
|
# self.button = button # True -> YES button, False -> NO button
|
||||||
|
if pin_correct:
|
||||||
|
self.ui.pin = None
|
||||||
|
else:
|
||||||
|
self.ui.pin = "444222"
|
||||||
|
|
||||||
|
def set_passphrase(self, passphrase):
|
||||||
|
self.ui.passphrase = Mnemonic.normalize_string(passphrase)
|
||||||
|
|
||||||
|
def set_mnemonic(self, mnemonic):
|
||||||
|
self.mnemonic = Mnemonic.normalize_string(mnemonic).split(" ")
|
||||||
|
|
||||||
|
def _raw_read(self):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
|
||||||
|
# if SCREENSHOT and self.debug:
|
||||||
|
# from PIL import Image
|
||||||
|
|
||||||
|
# layout = self.debug.state().layout
|
||||||
|
# im = Image.new("RGB", (128, 64))
|
||||||
|
# pix = im.load()
|
||||||
|
# for x in range(128):
|
||||||
|
# for y in range(64):
|
||||||
|
# rx, ry = 127 - x, 63 - y
|
||||||
|
# if (ord(layout[rx + (ry / 8) * 128]) & (1 << (ry % 8))) > 0:
|
||||||
|
# pix[x, y] = (255, 255, 255)
|
||||||
|
# im.save("scr%05d.png" % self.screenshot_id)
|
||||||
|
# self.screenshot_id += 1
|
||||||
|
|
||||||
|
resp = super()._raw_read()
|
||||||
|
resp = self._filter_message(resp)
|
||||||
|
self._check_request(resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def _raw_write(self, msg):
|
||||||
|
return super()._raw_write(self._filter_message(msg))
|
||||||
|
|
||||||
|
def _raise_unexpected_response(self, msg):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
|
||||||
|
start_at = max(self.current_response - EXPECTED_RESPONSES_CONTEXT_LINES, 0)
|
||||||
|
stop_at = min(
|
||||||
|
self.current_response + EXPECTED_RESPONSES_CONTEXT_LINES + 1,
|
||||||
|
len(self.expected_responses),
|
||||||
|
)
|
||||||
|
output = []
|
||||||
|
output.append("Expected responses:")
|
||||||
|
if start_at > 0:
|
||||||
|
output.append(" (...{} previous responses omitted)".format(start_at))
|
||||||
|
for i in range(start_at, stop_at):
|
||||||
|
exp = self.expected_responses[i]
|
||||||
|
prefix = " " if i != self.current_response else ">>> "
|
||||||
|
set_fields = {
|
||||||
|
key: value
|
||||||
|
for key, value in exp.__dict__.items()
|
||||||
|
if value is not None and value != []
|
||||||
|
}
|
||||||
|
oneline_str = ", ".join("{}={!r}".format(*i) for i in set_fields.items())
|
||||||
|
if len(oneline_str) < 60:
|
||||||
|
output.append(
|
||||||
|
"{}{}({})".format(prefix, exp.__class__.__name__, oneline_str)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
item = []
|
||||||
|
item.append("{}{}(".format(prefix, exp.__class__.__name__))
|
||||||
|
for key, value in set_fields.items():
|
||||||
|
item.append("{} {}={!r}".format(prefix, key, value))
|
||||||
|
item.append("{})".format(prefix))
|
||||||
|
output.append("\n".join(item))
|
||||||
|
if stop_at < len(self.expected_responses):
|
||||||
|
omitted = len(self.expected_responses) - stop_at
|
||||||
|
output.append(" (...{} following responses omitted)".format(omitted))
|
||||||
|
|
||||||
|
output.append("")
|
||||||
|
if msg is not None:
|
||||||
|
output.append("Actually received:")
|
||||||
|
output.append(protobuf.format_message(msg))
|
||||||
|
else:
|
||||||
|
output.append("This message was never received.")
|
||||||
|
raise AssertionError("\n".join(output))
|
||||||
|
|
||||||
|
def _check_request(self, msg):
|
||||||
|
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||||
|
if self.expected_responses is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.current_response >= len(self.expected_responses):
|
||||||
|
raise AssertionError(
|
||||||
|
"No more messages were expected, but we got:\n"
|
||||||
|
+ protobuf.format_message(msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = self.expected_responses[self.current_response]
|
||||||
|
|
||||||
|
if msg.__class__ != expected.__class__:
|
||||||
|
self._raise_unexpected_response(msg)
|
||||||
|
|
||||||
|
for field, value in expected.__dict__.items():
|
||||||
|
if value is None or value == []:
|
||||||
|
continue
|
||||||
|
if getattr(msg, field) != value:
|
||||||
|
self._raise_unexpected_response(msg)
|
||||||
|
|
||||||
|
self.current_response += 1
|
||||||
|
|
||||||
|
def mnemonic_callback(self, _):
|
||||||
|
word, pos = self.debug.read_recovery_word()
|
||||||
|
if word != "":
|
||||||
|
return word
|
||||||
|
if pos != 0:
|
||||||
|
return self.mnemonic[pos - 1]
|
||||||
|
|
||||||
|
raise RuntimeError("Unexpected call")
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def load_device_by_mnemonic(
|
||||||
|
client,
|
||||||
|
mnemonic,
|
||||||
|
pin,
|
||||||
|
passphrase_protection,
|
||||||
|
label,
|
||||||
|
language="english",
|
||||||
|
skip_checksum=False,
|
||||||
|
expand=False,
|
||||||
|
):
|
||||||
|
# Convert mnemonic to UTF8 NKFD
|
||||||
|
mnemonic = Mnemonic.normalize_string(mnemonic)
|
||||||
|
|
||||||
|
# Convert mnemonic to ASCII stream
|
||||||
|
mnemonic = mnemonic.encode()
|
||||||
|
|
||||||
|
m = Mnemonic("english")
|
||||||
|
|
||||||
|
if expand:
|
||||||
|
mnemonic = m.expand(mnemonic)
|
||||||
|
|
||||||
|
if not skip_checksum and not m.check(mnemonic):
|
||||||
|
raise ValueError("Invalid mnemonic checksum")
|
||||||
|
|
||||||
|
if client.features.initialized:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Device is initialized already. Call device.wipe() and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.call(
|
||||||
|
proto.LoadDevice(
|
||||||
|
mnemonic=mnemonic,
|
||||||
|
pin=pin,
|
||||||
|
passphrase_protection=passphrase_protection,
|
||||||
|
language=language,
|
||||||
|
label=label,
|
||||||
|
skip_checksum=skip_checksum,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client.init_device()
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def load_device_by_xprv(client, xprv, pin, passphrase_protection, label, language):
|
||||||
|
if client.features.initialized:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Device is initialized already. Call wipe_device() and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
if xprv[0:4] not in ("xprv", "tprv"):
|
||||||
|
raise ValueError("Unknown type of xprv")
|
||||||
|
|
||||||
|
if not 100 < len(xprv) < 112: # yes this is correct in Python
|
||||||
|
raise ValueError("Invalid length of xprv")
|
||||||
|
|
||||||
|
node = proto.HDNodeType()
|
||||||
|
data = tools.b58decode(xprv, None).hex()
|
||||||
|
|
||||||
|
if data[90:92] != "00":
|
||||||
|
raise ValueError("Contain invalid private key")
|
||||||
|
|
||||||
|
checksum = (tools.btc_hash(bytes.fromhex(data[:156]))[:4]).hex()
|
||||||
|
if checksum != data[156:]:
|
||||||
|
raise ValueError("Checksum doesn't match")
|
||||||
|
|
||||||
|
# version 0488ade4
|
||||||
|
# depth 00
|
||||||
|
# fingerprint 00000000
|
||||||
|
# child_num 00000000
|
||||||
|
# chaincode 873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508
|
||||||
|
# privkey 00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35
|
||||||
|
# checksum e77e9d71
|
||||||
|
|
||||||
|
node.depth = int(data[8:10], 16)
|
||||||
|
node.fingerprint = int(data[10:18], 16)
|
||||||
|
node.child_num = int(data[18:26], 16)
|
||||||
|
node.chain_code = bytes.fromhex(data[26:90])
|
||||||
|
node.private_key = bytes.fromhex(data[92:156]) # skip 0x00 indicating privkey
|
||||||
|
|
||||||
|
resp = client.call(
|
||||||
|
proto.LoadDevice(
|
||||||
|
node=node,
|
||||||
|
pin=pin,
|
||||||
|
passphrase_protection=passphrase_protection,
|
||||||
|
language=language,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client.init_device()
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def self_test(client):
|
||||||
|
if client.features.bootloader_mode is not True:
|
||||||
|
raise RuntimeError("Device must be in bootloader mode")
|
||||||
|
|
||||||
|
return client.call(
|
||||||
|
proto.SelfTest(
|
||||||
|
payload=b"\x00\xFF\x55\xAA\x66\x99\x33\xCCABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\x00\xFF\x55\xAA\x66\x99\x33\xCC"
|
||||||
|
)
|
||||||
|
)
|
210
python/trezorlib/device.py
Normal file
210
python/trezorlib/device.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .exceptions import Cancelled
|
||||||
|
from .tools import expect, session
|
||||||
|
from .transport import enumerate_devices, get_transport
|
||||||
|
|
||||||
|
RECOVERY_BACK = "\x08" # backspace character, sent literally
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorDevice:
|
||||||
|
"""
|
||||||
|
This class is deprecated. (There is no reason for it to exist in the first
|
||||||
|
place, it is nothing but a collection of two functions.)
|
||||||
|
Instead, please use functions from the ``trezorlib.transport`` module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enumerate(cls):
|
||||||
|
warnings.warn("TrezorDevice is deprecated.", DeprecationWarning)
|
||||||
|
return enumerate_devices()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_by_path(cls, path):
|
||||||
|
warnings.warn("TrezorDevice is deprecated.", DeprecationWarning)
|
||||||
|
return get_transport(path, prefix_search=False)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def apply_settings(
|
||||||
|
client,
|
||||||
|
label=None,
|
||||||
|
language=None,
|
||||||
|
use_passphrase=None,
|
||||||
|
homescreen=None,
|
||||||
|
passphrase_source=None,
|
||||||
|
auto_lock_delay_ms=None,
|
||||||
|
display_rotation=None,
|
||||||
|
):
|
||||||
|
settings = proto.ApplySettings()
|
||||||
|
if label is not None:
|
||||||
|
settings.label = label
|
||||||
|
if language:
|
||||||
|
settings.language = language
|
||||||
|
if use_passphrase is not None:
|
||||||
|
settings.use_passphrase = use_passphrase
|
||||||
|
if homescreen is not None:
|
||||||
|
settings.homescreen = homescreen
|
||||||
|
if passphrase_source is not None:
|
||||||
|
settings.passphrase_source = passphrase_source
|
||||||
|
if auto_lock_delay_ms is not None:
|
||||||
|
settings.auto_lock_delay_ms = auto_lock_delay_ms
|
||||||
|
if display_rotation is not None:
|
||||||
|
settings.display_rotation = display_rotation
|
||||||
|
|
||||||
|
out = client.call(settings)
|
||||||
|
client.init_device() # Reload Features
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def apply_flags(client, flags):
|
||||||
|
out = client.call(proto.ApplyFlags(flags=flags))
|
||||||
|
client.init_device() # Reload Features
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def change_pin(client, remove=False):
|
||||||
|
ret = client.call(proto.ChangePin(remove=remove))
|
||||||
|
client.init_device() # Re-read features
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def set_u2f_counter(client, u2f_counter):
|
||||||
|
ret = client.call(proto.SetU2FCounter(u2f_counter=u2f_counter))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def wipe(client):
|
||||||
|
ret = client.call(proto.WipeDevice())
|
||||||
|
client.init_device()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def recover(
|
||||||
|
client,
|
||||||
|
word_count=24,
|
||||||
|
passphrase_protection=False,
|
||||||
|
pin_protection=True,
|
||||||
|
label=None,
|
||||||
|
language="english",
|
||||||
|
input_callback=None,
|
||||||
|
type=proto.RecoveryDeviceType.ScrambledWords,
|
||||||
|
dry_run=False,
|
||||||
|
u2f_counter=None,
|
||||||
|
):
|
||||||
|
if client.features.model == "1" and input_callback is None:
|
||||||
|
raise RuntimeError("Input callback required for Trezor One")
|
||||||
|
|
||||||
|
if word_count not in (12, 18, 24):
|
||||||
|
raise ValueError("Invalid word count. Use 12/18/24")
|
||||||
|
|
||||||
|
if client.features.initialized and not dry_run:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Device already initialized. Call device.wipe() and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
if u2f_counter is None:
|
||||||
|
u2f_counter = int(time.time())
|
||||||
|
|
||||||
|
res = client.call(
|
||||||
|
proto.RecoveryDevice(
|
||||||
|
word_count=word_count,
|
||||||
|
passphrase_protection=bool(passphrase_protection),
|
||||||
|
pin_protection=bool(pin_protection),
|
||||||
|
label=label,
|
||||||
|
language=language,
|
||||||
|
enforce_wordlist=True,
|
||||||
|
type=type,
|
||||||
|
dry_run=dry_run,
|
||||||
|
u2f_counter=u2f_counter,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while isinstance(res, proto.WordRequest):
|
||||||
|
try:
|
||||||
|
inp = input_callback(res.type)
|
||||||
|
res = client.call(proto.WordAck(word=inp))
|
||||||
|
except Cancelled:
|
||||||
|
res = client.call(proto.Cancel())
|
||||||
|
|
||||||
|
client.init_device()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
@session
|
||||||
|
def reset(
|
||||||
|
client,
|
||||||
|
display_random=False,
|
||||||
|
strength=None,
|
||||||
|
passphrase_protection=False,
|
||||||
|
pin_protection=True,
|
||||||
|
label=None,
|
||||||
|
language="english",
|
||||||
|
u2f_counter=0,
|
||||||
|
skip_backup=False,
|
||||||
|
no_backup=False,
|
||||||
|
):
|
||||||
|
if client.features.initialized:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Device is initialized already. Call wipe_device() and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
if strength is None:
|
||||||
|
if client.features.model == "1":
|
||||||
|
strength = 256
|
||||||
|
else:
|
||||||
|
strength = 128
|
||||||
|
|
||||||
|
# Begin with device reset workflow
|
||||||
|
msg = proto.ResetDevice(
|
||||||
|
display_random=bool(display_random),
|
||||||
|
strength=strength,
|
||||||
|
passphrase_protection=bool(passphrase_protection),
|
||||||
|
pin_protection=bool(pin_protection),
|
||||||
|
language=language,
|
||||||
|
label=label,
|
||||||
|
u2f_counter=u2f_counter,
|
||||||
|
skip_backup=bool(skip_backup),
|
||||||
|
no_backup=bool(no_backup),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.call(msg)
|
||||||
|
if not isinstance(resp, proto.EntropyRequest):
|
||||||
|
raise RuntimeError("Invalid response, expected EntropyRequest")
|
||||||
|
|
||||||
|
external_entropy = os.urandom(32)
|
||||||
|
# LOG.debug("Computer generated entropy: " + external_entropy.hex())
|
||||||
|
ret = client.call(proto.EntropyAck(entropy=external_entropy))
|
||||||
|
client.init_device()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Success, field="message")
|
||||||
|
def backup(client):
|
||||||
|
ret = client.call(proto.BackupDevice())
|
||||||
|
return ret
|
108
python/trezorlib/ethereum.py
Normal file
108
python/trezorlib/ethereum.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .tools import CallException, expect, normalize_nfc, session
|
||||||
|
|
||||||
|
|
||||||
|
def int_to_big_endian(value):
|
||||||
|
return value.to_bytes((value.bit_length() + 7) // 8, "big")
|
||||||
|
|
||||||
|
|
||||||
|
# ====== Client functions ====== #
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.EthereumAddress, field="address")
|
||||||
|
def get_address(client, n, show_display=False, multisig=None):
|
||||||
|
return client.call(proto.EthereumGetAddress(address_n=n, show_display=show_display))
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.EthereumPublicKey)
|
||||||
|
def get_public_node(client, n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
proto.EthereumGetPublicKey(address_n=n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@session
|
||||||
|
def sign_tx(
|
||||||
|
client,
|
||||||
|
n,
|
||||||
|
nonce,
|
||||||
|
gas_price,
|
||||||
|
gas_limit,
|
||||||
|
to,
|
||||||
|
value,
|
||||||
|
data=None,
|
||||||
|
chain_id=None,
|
||||||
|
tx_type=None,
|
||||||
|
):
|
||||||
|
msg = proto.EthereumSignTx(
|
||||||
|
address_n=n,
|
||||||
|
nonce=int_to_big_endian(nonce),
|
||||||
|
gas_price=int_to_big_endian(gas_price),
|
||||||
|
gas_limit=int_to_big_endian(gas_limit),
|
||||||
|
value=int_to_big_endian(value),
|
||||||
|
)
|
||||||
|
|
||||||
|
if to:
|
||||||
|
msg.to = to
|
||||||
|
|
||||||
|
if data:
|
||||||
|
msg.data_length = len(data)
|
||||||
|
data, chunk = data[1024:], data[:1024]
|
||||||
|
msg.data_initial_chunk = chunk
|
||||||
|
|
||||||
|
if chain_id:
|
||||||
|
msg.chain_id = chain_id
|
||||||
|
|
||||||
|
if tx_type is not None:
|
||||||
|
msg.tx_type = tx_type
|
||||||
|
|
||||||
|
response = client.call(msg)
|
||||||
|
|
||||||
|
while response.data_length is not None:
|
||||||
|
data_length = response.data_length
|
||||||
|
data, chunk = data[data_length:], data[:data_length]
|
||||||
|
response = client.call(proto.EthereumTxAck(data_chunk=chunk))
|
||||||
|
|
||||||
|
# https://github.com/trezor/trezor-core/pull/311
|
||||||
|
# only signature bit returned. recalculate signature_v
|
||||||
|
if response.signature_v <= 1:
|
||||||
|
response.signature_v += 2 * chain_id + 35
|
||||||
|
|
||||||
|
return response.signature_v, response.signature_r, response.signature_s
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.EthereumMessageSignature)
|
||||||
|
def sign_message(client, n, message):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
return client.call(proto.EthereumSignMessage(address_n=n, message=message))
|
||||||
|
|
||||||
|
|
||||||
|
def verify_message(client, address, signature, message):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
try:
|
||||||
|
resp = client.call(
|
||||||
|
proto.EthereumVerifyMessage(
|
||||||
|
address=address, signature=signature, message=message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except CallException as e:
|
||||||
|
resp = e
|
||||||
|
if isinstance(resp, proto.Success):
|
||||||
|
return True
|
||||||
|
return False
|
51
python/trezorlib/exceptions.py
Normal file
51
python/trezorlib/exceptions.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorFailure(TrezorException):
|
||||||
|
def __init__(self, failure):
|
||||||
|
self.failure = failure
|
||||||
|
# TODO: this is backwards compatibility with tests. it should be changed
|
||||||
|
super().__init__(self.failure.code, self.failure.message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
from .messages import FailureType
|
||||||
|
|
||||||
|
types = {
|
||||||
|
getattr(FailureType, name): name
|
||||||
|
for name in dir(FailureType)
|
||||||
|
if not name.startswith("_")
|
||||||
|
}
|
||||||
|
if self.failure.message is not None:
|
||||||
|
return "{}: {}".format(types[self.failure.code], self.failure.message)
|
||||||
|
else:
|
||||||
|
return types[self.failure.code]
|
||||||
|
|
||||||
|
|
||||||
|
class PinException(TrezorException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Cancelled(TrezorException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OutdatedFirmwareError(TrezorException):
|
||||||
|
pass
|
435
python/trezorlib/firmware.py
Normal file
435
python/trezorlib/firmware.py
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable, List, NewType, Tuple
|
||||||
|
|
||||||
|
import construct as c
|
||||||
|
import ecdsa
|
||||||
|
import pyblake2
|
||||||
|
|
||||||
|
from . import cosi, messages, tools
|
||||||
|
|
||||||
|
V1_SIGNATURE_SLOTS = 3
|
||||||
|
V1_BOOTLOADER_KEYS = {
|
||||||
|
1: "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58",
|
||||||
|
2: "0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1",
|
||||||
|
3: "0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58",
|
||||||
|
4: "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a",
|
||||||
|
5: "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45",
|
||||||
|
}
|
||||||
|
|
||||||
|
V2_BOOTLOADER_KEYS = [
|
||||||
|
bytes.fromhex("c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f"),
|
||||||
|
bytes.fromhex("80d036b08739b846f4cb77593078deb25dc9487aedcf52e30b4fb7cd7024178a"),
|
||||||
|
bytes.fromhex("b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751"),
|
||||||
|
]
|
||||||
|
V2_BOOTLOADER_M = 2
|
||||||
|
V2_BOOTLOADER_N = 3
|
||||||
|
|
||||||
|
ONEV2_CHUNK_SIZE = 1024 * 64
|
||||||
|
V2_CHUNK_SIZE = 1024 * 128
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_vendor_trust(data: bytes) -> bytes:
|
||||||
|
"""Byte-swap and bit-invert the VendorTrust field.
|
||||||
|
|
||||||
|
Vendor trust is interpreted as a bitmask in a 16-bit little-endian integer,
|
||||||
|
with the added twist that 0 means set and 1 means unset.
|
||||||
|
We feed it to a `BitStruct` that expects a big-endian sequence where bits have
|
||||||
|
the traditional meaning. We must therefore do a bitwise negation of each byte,
|
||||||
|
and return them in reverse order. This is the same transformation both ways,
|
||||||
|
fortunately, so we don't need two separate functions.
|
||||||
|
"""
|
||||||
|
return bytes(~b & 0xFF for b in data)[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
class FirmwareIntegrityError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSignatureError(FirmwareIntegrityError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Unsigned(FirmwareIntegrityError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
Toif = c.Struct(
|
||||||
|
"magic" / c.Const(b"TOI"),
|
||||||
|
"format" / c.Enum(c.Byte, full_color=b"f", grayscale=b"g"),
|
||||||
|
"width" / c.Int16ul,
|
||||||
|
"height" / c.Int16ul,
|
||||||
|
"data" / c.Prefixed(c.Int32ul, c.GreedyBytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
VendorTrust = c.Transformed(c.BitStruct(
|
||||||
|
"reserved" / c.Default(c.BitsInteger(9), 0),
|
||||||
|
"show_vendor_string" / c.Flag,
|
||||||
|
"require_user_click" / c.Flag,
|
||||||
|
"red_background" / c.Flag,
|
||||||
|
"delay" / c.BitsInteger(4),
|
||||||
|
), _transform_vendor_trust, 2, _transform_vendor_trust, 2)
|
||||||
|
|
||||||
|
|
||||||
|
VendorHeader = c.Struct(
|
||||||
|
"_start_offset" / c.Tell,
|
||||||
|
"magic" / c.Const(b"TRZV"),
|
||||||
|
"_header_len" / c.Padding(4),
|
||||||
|
"expiry" / c.Int32ul,
|
||||||
|
"version" / c.Struct(
|
||||||
|
"major" / c.Int8ul,
|
||||||
|
"minor" / c.Int8ul,
|
||||||
|
),
|
||||||
|
"vendor_sigs_required" / c.Int8ul,
|
||||||
|
"vendor_sigs_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)),
|
||||||
|
"vendor_trust" / VendorTrust,
|
||||||
|
"reserved" / c.Padding(14),
|
||||||
|
"pubkeys" / c.Bytes(32)[c.this.vendor_sigs_n],
|
||||||
|
"vendor_string" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")),
|
||||||
|
"vendor_image" / Toif,
|
||||||
|
"_data_end_offset" / c.Tell,
|
||||||
|
|
||||||
|
c.Padding(-(c.this._data_end_offset + 65) % 512),
|
||||||
|
"sigmask" / c.Byte,
|
||||||
|
"signature" / c.Bytes(64),
|
||||||
|
|
||||||
|
"_end_offset" / c.Tell,
|
||||||
|
"header_len" / c.Pointer(
|
||||||
|
c.this._start_offset + 4,
|
||||||
|
c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
VersionLong = c.Struct(
|
||||||
|
"major" / c.Int8ul,
|
||||||
|
"minor" / c.Int8ul,
|
||||||
|
"patch" / c.Int8ul,
|
||||||
|
"build" / c.Int8ul,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FirmwareHeader = c.Struct(
|
||||||
|
"_start_offset" / c.Tell,
|
||||||
|
"magic" / c.Const(b"TRZF"),
|
||||||
|
"header_len" / c.Int32ul,
|
||||||
|
"expiry" / c.Int32ul,
|
||||||
|
"code_length" / c.Rebuild(
|
||||||
|
c.Int32ul,
|
||||||
|
lambda this:
|
||||||
|
len(this._.code) if "code" in this._
|
||||||
|
else (this.code_length or 0)
|
||||||
|
),
|
||||||
|
"version" / VersionLong,
|
||||||
|
"fix_version" / VersionLong,
|
||||||
|
"reserved" / c.Padding(8),
|
||||||
|
"hashes" / c.Bytes(32)[16],
|
||||||
|
|
||||||
|
"v1_signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS],
|
||||||
|
"v1_key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136
|
||||||
|
|
||||||
|
"reserved" / c.Padding(220),
|
||||||
|
"sigmask" / c.Byte,
|
||||||
|
"signature" / c.Bytes(64),
|
||||||
|
|
||||||
|
"_end_offset" / c.Tell,
|
||||||
|
|
||||||
|
"_rebuild_header_len" / c.If(
|
||||||
|
c.this.version.major > 1,
|
||||||
|
c.Pointer(
|
||||||
|
c.this._start_offset + 4,
|
||||||
|
c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Firmware = c.Struct(
|
||||||
|
"vendor_header" / VendorHeader,
|
||||||
|
"firmware_header" / FirmwareHeader,
|
||||||
|
"_code_offset" / c.Tell,
|
||||||
|
"code" / c.Bytes(c.this.firmware_header.code_length),
|
||||||
|
c.Terminated,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FirmwareOneV2 = c.Struct(
|
||||||
|
"firmware_header" / FirmwareHeader,
|
||||||
|
"_code_offset" / c.Tell,
|
||||||
|
"code" / c.Bytes(c.this.firmware_header.code_length),
|
||||||
|
c.Terminated,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FirmwareOne = c.Struct(
|
||||||
|
"magic" / c.Const(b"TRZR"),
|
||||||
|
"code_length" / c.Rebuild(c.Int32ul, c.len_(c.this.code)),
|
||||||
|
"key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136
|
||||||
|
"flags" / c.BitStruct(
|
||||||
|
c.Padding(7),
|
||||||
|
"restore_storage" / c.Flag,
|
||||||
|
),
|
||||||
|
"reserved" / c.Padding(52),
|
||||||
|
"signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS],
|
||||||
|
"code" / c.Bytes(c.this.code_length),
|
||||||
|
c.Terminated,
|
||||||
|
|
||||||
|
"embedded_onev2" / c.RestreamData(c.this.code, c.Optional(FirmwareOneV2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
class FirmwareFormat(Enum):
|
||||||
|
TREZOR_ONE = 1
|
||||||
|
TREZOR_T = 2
|
||||||
|
TREZOR_ONE_V2 = 3
|
||||||
|
|
||||||
|
|
||||||
|
FirmwareType = NewType("FirmwareType", c.Container)
|
||||||
|
ParsedFirmware = Tuple[FirmwareFormat, FirmwareType]
|
||||||
|
|
||||||
|
|
||||||
|
def parse(data: bytes) -> ParsedFirmware:
|
||||||
|
if data[:4] == b"TRZR":
|
||||||
|
version = FirmwareFormat.TREZOR_ONE
|
||||||
|
cls = FirmwareOne
|
||||||
|
elif data[:4] == b"TRZV":
|
||||||
|
version = FirmwareFormat.TREZOR_T
|
||||||
|
cls = Firmware
|
||||||
|
elif data[:4] == b"TRZF":
|
||||||
|
version = FirmwareFormat.TREZOR_ONE_V2
|
||||||
|
cls = FirmwareOneV2
|
||||||
|
else:
|
||||||
|
raise ValueError("Unrecognized firmware image type")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fw = cls.parse(data)
|
||||||
|
except Exception as e:
|
||||||
|
raise FirmwareIntegrityError("Invalid firmware image") from e
|
||||||
|
return version, FirmwareType(fw)
|
||||||
|
|
||||||
|
|
||||||
|
def digest_onev1(fw: FirmwareType) -> bytes:
|
||||||
|
return hashlib.sha256(fw.code).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def check_sig_v1(
|
||||||
|
digest: bytes, key_indexes: List[int], signatures: List[bytes]
|
||||||
|
) -> None:
|
||||||
|
distinct_key_indexes = set(i for i in key_indexes if i != 0)
|
||||||
|
if not distinct_key_indexes:
|
||||||
|
raise Unsigned
|
||||||
|
|
||||||
|
if len(distinct_key_indexes) < len(key_indexes):
|
||||||
|
raise InvalidSignatureError(
|
||||||
|
"Not enough distinct signatures (found {}, need {})".format(
|
||||||
|
len(distinct_key_indexes), len(key_indexes)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(len(key_indexes)):
|
||||||
|
key_idx = key_indexes[i]
|
||||||
|
signature = signatures[i]
|
||||||
|
|
||||||
|
if key_idx not in V1_BOOTLOADER_KEYS:
|
||||||
|
# unknown pubkey
|
||||||
|
raise InvalidSignatureError("Unknown key in slot {}".format(i))
|
||||||
|
|
||||||
|
pubkey = bytes.fromhex(V1_BOOTLOADER_KEYS[key_idx])[1:]
|
||||||
|
verify = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
|
||||||
|
try:
|
||||||
|
verify.verify_digest(signature, digest)
|
||||||
|
except ecdsa.BadSignatureError as e:
|
||||||
|
raise InvalidSignatureError("Invalid signature in slot {}".format(i)) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _header_digest(
|
||||||
|
header: c.Container,
|
||||||
|
header_type: c.Construct,
|
||||||
|
hash_function: Callable = pyblake2.blake2s,
|
||||||
|
) -> bytes:
|
||||||
|
stripped_header = header.copy()
|
||||||
|
stripped_header.sigmask = 0
|
||||||
|
stripped_header.signature = b"\0" * 64
|
||||||
|
stripped_header.v1_key_indexes = [0, 0, 0]
|
||||||
|
stripped_header.v1_signatures = [b"\0" * 64] * 3
|
||||||
|
header_bytes = header_type.build(stripped_header)
|
||||||
|
return hash_function(header_bytes).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def digest_v2(fw: FirmwareType) -> bytes:
|
||||||
|
return _header_digest(fw.firmware_header, FirmwareHeader, pyblake2.blake2s)
|
||||||
|
|
||||||
|
|
||||||
|
def digest_onev2(fw: FirmwareType) -> bytes:
|
||||||
|
return _header_digest(fw.firmware_header, FirmwareHeader, hashlib.sha256)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_code_hashes(
|
||||||
|
fw: FirmwareType,
|
||||||
|
hash_function: Callable = pyblake2.blake2s,
|
||||||
|
chunk_size: int = V2_CHUNK_SIZE,
|
||||||
|
padding_byte: bytes = None,
|
||||||
|
) -> None:
|
||||||
|
for i, expected_hash in enumerate(fw.firmware_header.hashes):
|
||||||
|
if i == 0:
|
||||||
|
# Because first chunk is sent along with headers, there is less code in it.
|
||||||
|
chunk = fw.code[: chunk_size - fw._code_offset]
|
||||||
|
else:
|
||||||
|
# Subsequent chunks are shifted by the "missing header" size.
|
||||||
|
ptr = i * chunk_size - fw._code_offset
|
||||||
|
chunk = fw.code[ptr : ptr + chunk_size]
|
||||||
|
|
||||||
|
# padding for last chunk
|
||||||
|
if padding_byte is not None and i > 1 and chunk and len(chunk) < chunk_size:
|
||||||
|
chunk += padding_byte[0:1] * (chunk_size - len(chunk))
|
||||||
|
|
||||||
|
if not chunk and expected_hash == b"\0" * 32:
|
||||||
|
continue
|
||||||
|
chunk_hash = hash_function(chunk).digest()
|
||||||
|
if chunk_hash != expected_hash:
|
||||||
|
raise FirmwareIntegrityError("Invalid firmware data.")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_onev2(fw: FirmwareType, allow_unsigned: bool = False) -> None:
|
||||||
|
try:
|
||||||
|
check_sig_v1(
|
||||||
|
digest_onev2(fw),
|
||||||
|
fw.firmware_header.v1_key_indexes,
|
||||||
|
fw.firmware_header.v1_signatures,
|
||||||
|
)
|
||||||
|
except Unsigned:
|
||||||
|
if not allow_unsigned:
|
||||||
|
raise
|
||||||
|
|
||||||
|
validate_code_hashes(
|
||||||
|
fw,
|
||||||
|
hash_function=hashlib.sha256,
|
||||||
|
chunk_size=ONEV2_CHUNK_SIZE,
|
||||||
|
padding_byte=b"\xFF",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_onev1(fw: FirmwareType, allow_unsigned: bool = False) -> None:
|
||||||
|
try:
|
||||||
|
check_sig_v1(digest_onev1(fw), fw.key_indexes, fw.signatures)
|
||||||
|
except Unsigned:
|
||||||
|
if not allow_unsigned:
|
||||||
|
raise
|
||||||
|
if fw.embedded_onev2:
|
||||||
|
validate_onev2(fw.embedded_onev2, allow_unsigned)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None:
|
||||||
|
vendor_fingerprint = _header_digest(fw.vendor_header, VendorHeader)
|
||||||
|
fingerprint = digest_v2(fw)
|
||||||
|
|
||||||
|
if not skip_vendor_header:
|
||||||
|
try:
|
||||||
|
# if you want to validate a custom vendor header, you can modify
|
||||||
|
# the global variables to match your keys and m-of-n scheme
|
||||||
|
cosi.verify_m_of_n(
|
||||||
|
fw.vendor_header.signature,
|
||||||
|
vendor_fingerprint,
|
||||||
|
V2_BOOTLOADER_M,
|
||||||
|
V2_BOOTLOADER_N,
|
||||||
|
fw.vendor_header.sigmask,
|
||||||
|
V2_BOOTLOADER_KEYS,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise InvalidSignatureError("Invalid vendor header signature.")
|
||||||
|
|
||||||
|
# XXX expiry is not used now
|
||||||
|
# now = time.gmtime()
|
||||||
|
# if time.gmtime(fw.vendor_header.expiry) < now:
|
||||||
|
# raise ValueError("Vendor header expired.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cosi.verify_m_of_n(
|
||||||
|
fw.firmware_header.signature,
|
||||||
|
fingerprint,
|
||||||
|
fw.vendor_header.vendor_sigs_required,
|
||||||
|
fw.vendor_header.vendor_sigs_n,
|
||||||
|
fw.firmware_header.sigmask,
|
||||||
|
fw.vendor_header.pubkeys,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise InvalidSignatureError("Invalid firmware signature.")
|
||||||
|
|
||||||
|
# XXX expiry is not used now
|
||||||
|
# if time.gmtime(fw.firmware_header.expiry) < now:
|
||||||
|
# raise ValueError("Firmware header expired.")
|
||||||
|
validate_code_hashes(fw)
|
||||||
|
|
||||||
|
|
||||||
|
def digest(version: FirmwareFormat, fw: FirmwareType) -> bytes:
|
||||||
|
if version == FirmwareFormat.TREZOR_ONE:
|
||||||
|
return digest_onev1(fw)
|
||||||
|
elif version == FirmwareFormat.TREZOR_ONE_V2:
|
||||||
|
return digest_onev2(fw)
|
||||||
|
elif version == FirmwareFormat.TREZOR_T:
|
||||||
|
return digest_v2(fw)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unrecognized firmware version")
|
||||||
|
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
version: FirmwareFormat, fw: FirmwareType, allow_unsigned: bool = False
|
||||||
|
) -> None:
|
||||||
|
if version == FirmwareFormat.TREZOR_ONE:
|
||||||
|
return validate_onev1(fw, allow_unsigned)
|
||||||
|
elif version == FirmwareFormat.TREZOR_ONE_V2:
|
||||||
|
return validate_onev2(fw, allow_unsigned)
|
||||||
|
elif version == FirmwareFormat.TREZOR_T:
|
||||||
|
return validate_v2(fw)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unrecognized firmware version")
|
||||||
|
|
||||||
|
|
||||||
|
# ====== Client functions ====== #
|
||||||
|
|
||||||
|
|
||||||
|
@tools.session
|
||||||
|
def update(client, data):
|
||||||
|
if client.features.bootloader_mode is False:
|
||||||
|
raise RuntimeError("Device must be in bootloader mode")
|
||||||
|
|
||||||
|
resp = client.call(messages.FirmwareErase(length=len(data)))
|
||||||
|
|
||||||
|
# TREZORv1 method
|
||||||
|
if isinstance(resp, messages.Success):
|
||||||
|
resp = client.call(messages.FirmwareUpload(payload=data))
|
||||||
|
if isinstance(resp, messages.Success):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unexpected result %s" % resp)
|
||||||
|
|
||||||
|
# TREZORv2 method
|
||||||
|
while isinstance(resp, messages.FirmwareRequest):
|
||||||
|
payload = data[resp.offset : resp.offset + resp.length]
|
||||||
|
digest = pyblake2.blake2s(payload).digest()
|
||||||
|
resp = client.call(messages.FirmwareUpload(payload=payload, hash=digest))
|
||||||
|
|
||||||
|
if isinstance(resp, messages.Success):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unexpected message %s" % resp)
|
58
python/trezorlib/lisk.py
Normal file
58
python/trezorlib/lisk.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .protobuf import dict_to_proto
|
||||||
|
from .tools import CallException, dict_from_camelcase, expect, normalize_nfc
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.LiskAddress, field="address")
|
||||||
|
def get_address(client, n, show_display=False):
|
||||||
|
return client.call(proto.LiskGetAddress(address_n=n, show_display=show_display))
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.LiskPublicKey)
|
||||||
|
def get_public_key(client, n, show_display=False):
|
||||||
|
return client.call(proto.LiskGetPublicKey(address_n=n, show_display=show_display))
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.LiskMessageSignature)
|
||||||
|
def sign_message(client, n, message):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
return client.call(proto.LiskSignMessage(address_n=n, message=message))
|
||||||
|
|
||||||
|
|
||||||
|
def verify_message(client, pubkey, signature, message):
|
||||||
|
message = normalize_nfc(message)
|
||||||
|
try:
|
||||||
|
resp = client.call(
|
||||||
|
proto.LiskVerifyMessage(
|
||||||
|
signature=signature, public_key=pubkey, message=message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except CallException as e:
|
||||||
|
resp = e
|
||||||
|
return isinstance(resp, proto.Success)
|
||||||
|
|
||||||
|
|
||||||
|
RENAMES = {"lifetime": "life_time", "keysgroup": "keys_group"}
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.LiskSignedTx)
|
||||||
|
def sign_tx(client, n, transaction):
|
||||||
|
transaction = dict_from_camelcase(transaction, renames=RENAMES)
|
||||||
|
msg = dict_to_proto(proto.LiskTransactionCommon, transaction)
|
||||||
|
return client.call(proto.LiskSignTx(address_n=n, transaction=msg))
|
51
python/trezorlib/log.py
Normal file
51
python/trezorlib/log.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
|
from . import protobuf
|
||||||
|
|
||||||
|
OMITTED_MESSAGES = set() # type: Set[Type[protobuf.MessageType]]
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyProtobufFormatter(logging.Formatter):
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
time = self.formatTime(record)
|
||||||
|
message = "[{time}] {source} {level}: {msg}".format(
|
||||||
|
time=time,
|
||||||
|
level=record.levelname.upper(),
|
||||||
|
source=record.name,
|
||||||
|
msg=super().format(record),
|
||||||
|
)
|
||||||
|
if hasattr(record, "protobuf"):
|
||||||
|
if type(record.protobuf) in OMITTED_MESSAGES:
|
||||||
|
message += " ({} bytes)".format(record.protobuf.ByteSize())
|
||||||
|
else:
|
||||||
|
message += "\n" + protobuf.format_message(record.protobuf)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def enable_debug_output(handler: Optional[logging.Handler] = None):
|
||||||
|
if handler is None:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
|
||||||
|
formatter = PrettyProtobufFormatter()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger = logging.getLogger("trezorlib")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(handler)
|
62
python/trezorlib/mapping.py
Normal file
62
python/trezorlib/mapping.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages
|
||||||
|
|
||||||
|
map_type_to_class = {}
|
||||||
|
map_class_to_type = {}
|
||||||
|
|
||||||
|
|
||||||
|
def build_map():
|
||||||
|
for msg_name in dir(messages.MessageType):
|
||||||
|
if msg_name.startswith("__"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg_class = getattr(messages, msg_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(
|
||||||
|
"Implementation of protobuf message '%s' is missing" % msg_name
|
||||||
|
)
|
||||||
|
|
||||||
|
if msg_class.MESSAGE_WIRE_TYPE != getattr(messages.MessageType, msg_name):
|
||||||
|
raise ValueError(
|
||||||
|
"Inconsistent wire type and MessageType record for '%s'" % msg_class
|
||||||
|
)
|
||||||
|
|
||||||
|
register_message(msg_class)
|
||||||
|
|
||||||
|
|
||||||
|
def register_message(msg_class):
|
||||||
|
if msg_class.MESSAGE_WIRE_TYPE in map_type_to_class:
|
||||||
|
raise Exception(
|
||||||
|
"Message for wire type %s is already registered by %s"
|
||||||
|
% (msg_class.MESSAGE_WIRE_TYPE, get_class(msg_class.MESSAGE_WIRE_TYPE))
|
||||||
|
)
|
||||||
|
|
||||||
|
map_class_to_type[msg_class] = msg_class.MESSAGE_WIRE_TYPE
|
||||||
|
map_type_to_class[msg_class.MESSAGE_WIRE_TYPE] = msg_class
|
||||||
|
|
||||||
|
|
||||||
|
def get_type(msg):
|
||||||
|
return map_class_to_type[msg.__class__]
|
||||||
|
|
||||||
|
|
||||||
|
def get_class(t):
|
||||||
|
return map_type_to_class[t]
|
||||||
|
|
||||||
|
|
||||||
|
build_map()
|
0
python/trezorlib/messages/.keep
Normal file
0
python/trezorlib/messages/.keep
Normal file
82
python/trezorlib/misc.py
Normal file
82
python/trezorlib/misc.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .tools import expect
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.Entropy, field="entropy")
|
||||||
|
def get_entropy(client, size):
|
||||||
|
return client.call(proto.GetEntropy(size=size))
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.SignedIdentity)
|
||||||
|
def sign_identity(
|
||||||
|
client, identity, challenge_hidden, challenge_visual, ecdsa_curve_name=None
|
||||||
|
):
|
||||||
|
return client.call(
|
||||||
|
proto.SignIdentity(
|
||||||
|
identity=identity,
|
||||||
|
challenge_hidden=challenge_hidden,
|
||||||
|
challenge_visual=challenge_visual,
|
||||||
|
ecdsa_curve_name=ecdsa_curve_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.ECDHSessionKey)
|
||||||
|
def get_ecdh_session_key(client, identity, peer_public_key, ecdsa_curve_name=None):
|
||||||
|
return client.call(
|
||||||
|
proto.GetECDHSessionKey(
|
||||||
|
identity=identity,
|
||||||
|
peer_public_key=peer_public_key,
|
||||||
|
ecdsa_curve_name=ecdsa_curve_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.CipheredKeyValue, field="value")
|
||||||
|
def encrypt_keyvalue(
|
||||||
|
client, n, key, value, ask_on_encrypt=True, ask_on_decrypt=True, iv=b""
|
||||||
|
):
|
||||||
|
return client.call(
|
||||||
|
proto.CipherKeyValue(
|
||||||
|
address_n=n,
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
encrypt=True,
|
||||||
|
ask_on_encrypt=ask_on_encrypt,
|
||||||
|
ask_on_decrypt=ask_on_decrypt,
|
||||||
|
iv=iv,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.CipheredKeyValue, field="value")
|
||||||
|
def decrypt_keyvalue(
|
||||||
|
client, n, key, value, ask_on_encrypt=True, ask_on_decrypt=True, iv=b""
|
||||||
|
):
|
||||||
|
return client.call(
|
||||||
|
proto.CipherKeyValue(
|
||||||
|
address_n=n,
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
encrypt=False,
|
||||||
|
ask_on_encrypt=ask_on_encrypt,
|
||||||
|
ask_on_decrypt=ask_on_decrypt,
|
||||||
|
iv=iv,
|
||||||
|
)
|
||||||
|
)
|
37
python/trezorlib/monero.py
Normal file
37
python/trezorlib/monero.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .tools import expect
|
||||||
|
|
||||||
|
# MAINNET = 0
|
||||||
|
# TESTNET = 1
|
||||||
|
# STAGENET = 2
|
||||||
|
# FAKECHAIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.MoneroAddress, field="address")
|
||||||
|
def get_address(client, n, show_display=False, network_type=0):
|
||||||
|
return client.call(
|
||||||
|
proto.MoneroGetAddress(
|
||||||
|
address_n=n, show_display=show_display, network_type=network_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.MoneroWatchKey)
|
||||||
|
def get_watch_key(client, n, network_type=0):
|
||||||
|
return client.call(proto.MoneroGetWatchKey(address_n=n, network_type=network_type))
|
200
python/trezorlib/nem.py
Normal file
200
python/trezorlib/nem.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from . import messages as proto
|
||||||
|
from .tools import CallException, expect
|
||||||
|
|
||||||
|
TYPE_TRANSACTION_TRANSFER = 0x0101
|
||||||
|
TYPE_IMPORTANCE_TRANSFER = 0x0801
|
||||||
|
TYPE_AGGREGATE_MODIFICATION = 0x1001
|
||||||
|
TYPE_MULTISIG_SIGNATURE = 0x1002
|
||||||
|
TYPE_MULTISIG = 0x1004
|
||||||
|
TYPE_PROVISION_NAMESPACE = 0x2001
|
||||||
|
TYPE_MOSAIC_CREATION = 0x4001
|
||||||
|
TYPE_MOSAIC_SUPPLY_CHANGE = 0x4002
|
||||||
|
|
||||||
|
|
||||||
|
def create_transaction_common(transaction):
|
||||||
|
msg = proto.NEMTransactionCommon()
|
||||||
|
msg.network = (transaction["version"] >> 24) & 0xFF
|
||||||
|
msg.timestamp = transaction["timeStamp"]
|
||||||
|
msg.fee = transaction["fee"]
|
||||||
|
msg.deadline = transaction["deadline"]
|
||||||
|
|
||||||
|
if "signer" in transaction:
|
||||||
|
msg.signer = bytes.fromhex(transaction["signer"])
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_transfer(transaction):
|
||||||
|
msg = proto.NEMTransfer()
|
||||||
|
msg.recipient = transaction["recipient"]
|
||||||
|
msg.amount = transaction["amount"]
|
||||||
|
|
||||||
|
if "payload" in transaction["message"]:
|
||||||
|
msg.payload = bytes.fromhex(transaction["message"]["payload"])
|
||||||
|
|
||||||
|
if transaction["message"]["type"] == 0x02:
|
||||||
|
msg.public_key = bytes.fromhex(transaction["message"]["publicKey"])
|
||||||
|
|
||||||
|
if "mosaics" in transaction:
|
||||||
|
msg.mosaics = [
|
||||||
|
proto.NEMMosaic(
|
||||||
|
namespace=mosaic["mosaicId"]["namespaceId"],
|
||||||
|
mosaic=mosaic["mosaicId"]["name"],
|
||||||
|
quantity=mosaic["quantity"],
|
||||||
|
)
|
||||||
|
for mosaic in transaction["mosaics"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_aggregate_modification(transactions):
|
||||||
|
msg = proto.NEMAggregateModification()
|
||||||
|
msg.modifications = [
|
||||||
|
proto.NEMCosignatoryModification(
|
||||||
|
type=modification["modificationType"],
|
||||||
|
public_key=bytes.fromhex(modification["cosignatoryAccount"]),
|
||||||
|
)
|
||||||
|
for modification in transactions["modifications"]
|
||||||
|
]
|
||||||
|
|
||||||
|
if "minCosignatories" in transactions:
|
||||||
|
msg.relative_change = transactions["minCosignatories"]["relativeChange"]
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_provision_namespace(transaction):
|
||||||
|
msg = proto.NEMProvisionNamespace()
|
||||||
|
msg.namespace = transaction["newPart"]
|
||||||
|
|
||||||
|
if transaction["parent"]:
|
||||||
|
msg.parent = transaction["parent"]
|
||||||
|
|
||||||
|
msg.sink = transaction["rentalFeeSink"]
|
||||||
|
msg.fee = transaction["rentalFee"]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_mosaic_creation(transaction):
|
||||||
|
definition = transaction["mosaicDefinition"]
|
||||||
|
msg = proto.NEMMosaicCreation()
|
||||||
|
msg.definition = proto.NEMMosaicDefinition()
|
||||||
|
msg.definition.namespace = definition["id"]["namespaceId"]
|
||||||
|
msg.definition.mosaic = definition["id"]["name"]
|
||||||
|
|
||||||
|
if definition["levy"]:
|
||||||
|
msg.definition.levy = definition["levy"]["type"]
|
||||||
|
msg.definition.fee = definition["levy"]["fee"]
|
||||||
|
msg.definition.levy_address = definition["levy"]["recipient"]
|
||||||
|
msg.definition.levy_namespace = definition["levy"]["mosaicId"]["namespaceId"]
|
||||||
|
msg.definition.levy_mosaic = definition["levy"]["mosaicId"]["name"]
|
||||||
|
|
||||||
|
msg.definition.description = definition["description"]
|
||||||
|
|
||||||
|
for property in definition["properties"]:
|
||||||
|
name = property["name"]
|
||||||
|
value = json.loads(property["value"])
|
||||||
|
|
||||||
|
if name == "divisibility":
|
||||||
|
msg.definition.divisibility = value
|
||||||
|
elif name == "initialSupply":
|
||||||
|
msg.definition.supply = value
|
||||||
|
elif name == "supplyMutable":
|
||||||
|
msg.definition.mutable_supply = value
|
||||||
|
elif name == "transferable":
|
||||||
|
msg.definition.transferable = value
|
||||||
|
|
||||||
|
msg.sink = transaction["creationFeeSink"]
|
||||||
|
msg.fee = transaction["creationFee"]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_supply_change(transaction):
|
||||||
|
msg = proto.NEMMosaicSupplyChange()
|
||||||
|
msg.namespace = transaction["mosaicId"]["namespaceId"]
|
||||||
|
msg.mosaic = transaction["mosaicId"]["name"]
|
||||||
|
msg.type = transaction["supplyType"]
|
||||||
|
msg.delta = transaction["delta"]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_importance_transfer(transaction):
|
||||||
|
msg = proto.NEMImportanceTransfer()
|
||||||
|
msg.mode = transaction["importanceTransfer"]["mode"]
|
||||||
|
msg.public_key = bytes.fromhex(transaction["importanceTransfer"]["publicKey"])
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def fill_transaction_by_type(msg, transaction):
|
||||||
|
if transaction["type"] == TYPE_TRANSACTION_TRANSFER:
|
||||||
|
msg.transfer = create_transfer(transaction)
|
||||||
|
elif transaction["type"] == TYPE_AGGREGATE_MODIFICATION:
|
||||||
|
msg.aggregate_modification = create_aggregate_modification(transaction)
|
||||||
|
elif transaction["type"] == TYPE_PROVISION_NAMESPACE:
|
||||||
|
msg.provision_namespace = create_provision_namespace(transaction)
|
||||||
|
elif transaction["type"] == TYPE_MOSAIC_CREATION:
|
||||||
|
msg.mosaic_creation = create_mosaic_creation(transaction)
|
||||||
|
elif transaction["type"] == TYPE_MOSAIC_SUPPLY_CHANGE:
|
||||||
|
msg.supply_change = create_supply_change(transaction)
|
||||||
|
elif transaction["type"] == TYPE_IMPORTANCE_TRANSFER:
|
||||||
|
msg.importance_transfer = create_importance_transfer(transaction)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown transaction type")
|
||||||
|
|
||||||
|
|
||||||
|
def create_sign_tx(transaction):
|
||||||
|
msg = proto.NEMSignTx()
|
||||||
|
msg.transaction = create_transaction_common(transaction)
|
||||||
|
msg.cosigning = transaction["type"] == TYPE_MULTISIG_SIGNATURE
|
||||||
|
|
||||||
|
if transaction["type"] in (TYPE_MULTISIG_SIGNATURE, TYPE_MULTISIG):
|
||||||
|
other_trans = transaction["otherTrans"]
|
||||||
|
msg.multisig = create_transaction_common(other_trans)
|
||||||
|
fill_transaction_by_type(msg, other_trans)
|
||||||
|
elif "otherTrans" in transaction:
|
||||||
|
raise ValueError("Transaction does not support inner transaction")
|
||||||
|
else:
|
||||||
|
fill_transaction_by_type(msg, transaction)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# ====== Client functions ====== #
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.NEMAddress, field="address")
|
||||||
|
def get_address(client, n, network, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
proto.NEMGetAddress(address_n=n, network=network, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(proto.NEMSignedTx)
|
||||||
|
def sign_tx(client, n, transaction):
|
||||||
|
try:
|
||||||
|
msg = create_sign_tx(transaction)
|
||||||
|
except ValueError as e:
|
||||||
|
raise CallException(e.args)
|
||||||
|
|
||||||
|
assert msg.transaction is not None
|
||||||
|
msg.transaction.address_n = n
|
||||||
|
return client.call(msg)
|
70
python/trezorlib/ontology.py
Normal file
70
python/trezorlib/ontology.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages
|
||||||
|
from .tools import expect
|
||||||
|
|
||||||
|
#
|
||||||
|
# Ontology functions
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologyAddress, field="address")
|
||||||
|
def get_address(client, address_n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologyGetAddress(address_n=address_n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologyPublicKey)
|
||||||
|
def get_public_key(client, address_n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologyGetPublicKey(address_n=address_n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologySignedTransfer)
|
||||||
|
def sign_transfer(client, address_n, t, tr):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologySignTransfer(address_n=address_n, transaction=t, transfer=tr)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologySignedWithdrawOng)
|
||||||
|
def sign_withdrawal(client, address_n, t, w):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologySignWithdrawOng(
|
||||||
|
address_n=address_n, transaction=t, withdraw_ong=w
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologySignedOntIdRegister)
|
||||||
|
def sign_register(client, address_n, t, r):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologySignOntIdRegister(
|
||||||
|
address_n=address_n, transaction=t, ont_id_register=r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.OntologySignedOntIdAddAttributes)
|
||||||
|
def sign_add_attr(client, address_n, t, a):
|
||||||
|
return client.call(
|
||||||
|
messages.OntologySignOntIdAddAttributes(
|
||||||
|
address_n=address_n, transaction=t, ont_id_add_attributes=a
|
||||||
|
)
|
||||||
|
)
|
431
python/trezorlib/protobuf.py
Normal file
431
python/trezorlib/protobuf.py
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Extremely minimal streaming codec for a subset of protobuf. Supports uint32,
|
||||||
|
bytes, string, embedded message and repeated fields.
|
||||||
|
|
||||||
|
For de-sererializing (loading) protobuf types, object with `Reader`
|
||||||
|
interface is required:
|
||||||
|
|
||||||
|
>>> class Reader:
|
||||||
|
>>> def readinto(self, buffer):
|
||||||
|
>>> """
|
||||||
|
>>> Reads `len(buffer)` bytes into `buffer`, or raises `EOFError`.
|
||||||
|
>>> """
|
||||||
|
|
||||||
|
For serializing (dumping) protobuf types, object with `Writer` interface is
|
||||||
|
required:
|
||||||
|
|
||||||
|
>>> class Writer:
|
||||||
|
>>> def write(self, buffer):
|
||||||
|
>>> """
|
||||||
|
>>> Writes all bytes from `buffer`, or raises `EOFError`.
|
||||||
|
>>> """
|
||||||
|
'''
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
_UVARINT_BUFFER = bytearray(1)
|
||||||
|
|
||||||
|
|
||||||
|
def load_uvarint(reader):
|
||||||
|
buffer = _UVARINT_BUFFER
|
||||||
|
result = 0
|
||||||
|
shift = 0
|
||||||
|
byte = 0x80
|
||||||
|
while byte & 0x80:
|
||||||
|
if reader.readinto(buffer) == 0:
|
||||||
|
raise EOFError
|
||||||
|
byte = buffer[0]
|
||||||
|
result += (byte & 0x7F) << shift
|
||||||
|
shift += 7
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def dump_uvarint(writer, n):
|
||||||
|
if n < 0:
|
||||||
|
raise ValueError("Cannot dump signed value, convert it to unsigned first.")
|
||||||
|
buffer = _UVARINT_BUFFER
|
||||||
|
shifted = True
|
||||||
|
while shifted:
|
||||||
|
shifted = n >> 7
|
||||||
|
buffer[0] = (n & 0x7F) | (0x80 if shifted else 0x00)
|
||||||
|
writer.write(buffer)
|
||||||
|
n = shifted
|
||||||
|
|
||||||
|
|
||||||
|
# protobuf interleaved signed encoding:
|
||||||
|
# https://developers.google.com/protocol-buffers/docs/encoding#structure
|
||||||
|
# the idea is to save the sign in LSbit instead of twos-complement.
|
||||||
|
# so counting up, you go: 0, -1, 1, -2, 2, ... (as the first bit changes, sign flips)
|
||||||
|
#
|
||||||
|
# To achieve this with a twos-complement number:
|
||||||
|
# 1. shift left by 1, leaving LSbit free
|
||||||
|
# 2. if the number is negative, do bitwise negation.
|
||||||
|
# This keeps positive number the same, and converts negative from twos-complement
|
||||||
|
# to the appropriate value, while setting the sign bit.
|
||||||
|
#
|
||||||
|
# The original algorithm makes use of the fact that arithmetic (signed) shift
|
||||||
|
# keeps the sign bits, so for a n-bit number, (x >> n) gets us "all sign bits".
|
||||||
|
# Then you can take "number XOR all-sign-bits", which is XOR 0 (identity) for positive
|
||||||
|
# and XOR 1 (bitwise negation) for negative. Cute and efficient.
|
||||||
|
#
|
||||||
|
# But this is harder in Python because we don't natively know the bit size of the number.
|
||||||
|
# So we have to branch on whether the number is negative.
|
||||||
|
|
||||||
|
|
||||||
|
def sint_to_uint(sint):
|
||||||
|
res = sint << 1
|
||||||
|
if sint < 0:
|
||||||
|
res = ~res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def uint_to_sint(uint):
|
||||||
|
sign = uint & 1
|
||||||
|
res = uint >> 1
|
||||||
|
if sign:
|
||||||
|
res = ~res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class UVarintType:
|
||||||
|
WIRE_TYPE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class SVarintType:
|
||||||
|
WIRE_TYPE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BoolType:
|
||||||
|
WIRE_TYPE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BytesType:
|
||||||
|
WIRE_TYPE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeType:
|
||||||
|
WIRE_TYPE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class MessageType:
|
||||||
|
WIRE_TYPE = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for kw in kwargs:
|
||||||
|
setattr(self, kw, kwargs[kw])
|
||||||
|
self._fill_missing()
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return self.__class__ is rhs.__class__ and self.__dict__ == rhs.__dict__
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
d = {}
|
||||||
|
for key, value in self.__dict__.items():
|
||||||
|
if value is None or value == []:
|
||||||
|
continue
|
||||||
|
d[key] = value
|
||||||
|
return "<%s: %s>" % (self.__class__.__name__, d)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.keys())
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return (name for name, _, _ in self.get_fields().values())
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
def _fill_missing(self):
|
||||||
|
# fill missing fields
|
||||||
|
for fname, ftype, fflags in self.get_fields().values():
|
||||||
|
if not hasattr(self, fname):
|
||||||
|
if fflags & FLAG_REPEATED:
|
||||||
|
setattr(self, fname, [])
|
||||||
|
else:
|
||||||
|
setattr(self, fname, None)
|
||||||
|
|
||||||
|
def ByteSize(self):
|
||||||
|
data = BytesIO()
|
||||||
|
dump_message(data, self)
|
||||||
|
return len(data.getvalue())
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedReader:
|
||||||
|
def __init__(self, reader, limit):
|
||||||
|
self.reader = reader
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
def readinto(self, buf):
|
||||||
|
if self.limit < len(buf):
|
||||||
|
raise EOFError
|
||||||
|
else:
|
||||||
|
nread = self.reader.readinto(buf)
|
||||||
|
self.limit -= nread
|
||||||
|
return nread
|
||||||
|
|
||||||
|
|
||||||
|
class CountingWriter:
|
||||||
|
def __init__(self):
|
||||||
|
self.size = 0
|
||||||
|
|
||||||
|
def write(self, buf):
|
||||||
|
nwritten = len(buf)
|
||||||
|
self.size += nwritten
|
||||||
|
return nwritten
|
||||||
|
|
||||||
|
|
||||||
|
FLAG_REPEATED = 1
|
||||||
|
|
||||||
|
|
||||||
|
def load_message(reader, msg_type):
|
||||||
|
fields = msg_type.get_fields()
|
||||||
|
msg = msg_type()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
fkey = load_uvarint(reader)
|
||||||
|
except EOFError:
|
||||||
|
break # no more fields to load
|
||||||
|
|
||||||
|
ftag = fkey >> 3
|
||||||
|
wtype = fkey & 7
|
||||||
|
|
||||||
|
field = fields.get(ftag, None)
|
||||||
|
|
||||||
|
if field is None: # unknown field, skip it
|
||||||
|
if wtype == 0:
|
||||||
|
load_uvarint(reader)
|
||||||
|
elif wtype == 2:
|
||||||
|
ivalue = load_uvarint(reader)
|
||||||
|
reader.readinto(bytearray(ivalue))
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
continue
|
||||||
|
|
||||||
|
fname, ftype, fflags = field
|
||||||
|
if wtype != ftype.WIRE_TYPE:
|
||||||
|
raise TypeError # parsed wire type differs from the schema
|
||||||
|
|
||||||
|
ivalue = load_uvarint(reader)
|
||||||
|
|
||||||
|
if ftype is UVarintType:
|
||||||
|
fvalue = ivalue
|
||||||
|
elif ftype is SVarintType:
|
||||||
|
fvalue = uint_to_sint(ivalue)
|
||||||
|
elif ftype is BoolType:
|
||||||
|
fvalue = bool(ivalue)
|
||||||
|
elif ftype is BytesType:
|
||||||
|
buf = bytearray(ivalue)
|
||||||
|
reader.readinto(buf)
|
||||||
|
fvalue = bytes(buf)
|
||||||
|
elif ftype is UnicodeType:
|
||||||
|
buf = bytearray(ivalue)
|
||||||
|
reader.readinto(buf)
|
||||||
|
fvalue = buf.decode()
|
||||||
|
elif issubclass(ftype, MessageType):
|
||||||
|
fvalue = load_message(LimitedReader(reader, ivalue), ftype)
|
||||||
|
else:
|
||||||
|
raise TypeError # field type is unknown
|
||||||
|
|
||||||
|
if fflags & FLAG_REPEATED:
|
||||||
|
pvalue = getattr(msg, fname)
|
||||||
|
pvalue.append(fvalue)
|
||||||
|
fvalue = pvalue
|
||||||
|
setattr(msg, fname, fvalue)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def dump_message(writer, msg):
|
||||||
|
repvalue = [0]
|
||||||
|
mtype = msg.__class__
|
||||||
|
fields = mtype.get_fields()
|
||||||
|
|
||||||
|
for ftag in fields:
|
||||||
|
fname, ftype, fflags = fields[ftag]
|
||||||
|
|
||||||
|
fvalue = getattr(msg, fname, None)
|
||||||
|
if fvalue is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
fkey = (ftag << 3) | ftype.WIRE_TYPE
|
||||||
|
|
||||||
|
if not fflags & FLAG_REPEATED:
|
||||||
|
repvalue[0] = fvalue
|
||||||
|
fvalue = repvalue
|
||||||
|
|
||||||
|
for svalue in fvalue:
|
||||||
|
dump_uvarint(writer, fkey)
|
||||||
|
|
||||||
|
if ftype is UVarintType:
|
||||||
|
dump_uvarint(writer, svalue)
|
||||||
|
|
||||||
|
elif ftype is SVarintType:
|
||||||
|
dump_uvarint(writer, sint_to_uint(svalue))
|
||||||
|
|
||||||
|
elif ftype is BoolType:
|
||||||
|
dump_uvarint(writer, int(svalue))
|
||||||
|
|
||||||
|
elif ftype is BytesType:
|
||||||
|
dump_uvarint(writer, len(svalue))
|
||||||
|
writer.write(svalue)
|
||||||
|
|
||||||
|
elif ftype is UnicodeType:
|
||||||
|
if not isinstance(svalue, bytes):
|
||||||
|
svalue = svalue.encode()
|
||||||
|
|
||||||
|
dump_uvarint(writer, len(svalue))
|
||||||
|
writer.write(svalue)
|
||||||
|
|
||||||
|
elif issubclass(ftype, MessageType):
|
||||||
|
counter = CountingWriter()
|
||||||
|
dump_message(counter, svalue)
|
||||||
|
dump_uvarint(writer, counter.size)
|
||||||
|
dump_message(writer, svalue)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
|
||||||
|
def format_message(
|
||||||
|
pb: MessageType,
|
||||||
|
indent: int = 0,
|
||||||
|
sep: str = " " * 4,
|
||||||
|
truncate_after: Optional[int] = 256,
|
||||||
|
truncate_to: Optional[int] = 64,
|
||||||
|
) -> str:
|
||||||
|
def mostly_printable(bytes):
|
||||||
|
if not bytes:
|
||||||
|
return True
|
||||||
|
printable = sum(1 for byte in bytes if 0x20 <= byte <= 0x7E)
|
||||||
|
return printable / len(bytes) > 0.8
|
||||||
|
|
||||||
|
def pformat_value(value: Any, indent: int) -> str:
|
||||||
|
level = sep * indent
|
||||||
|
leadin = sep * (indent + 1)
|
||||||
|
if isinstance(value, MessageType):
|
||||||
|
return format_message(value, indent, sep)
|
||||||
|
if isinstance(value, list):
|
||||||
|
# short list of simple values
|
||||||
|
if not value or not isinstance(value[0], MessageType):
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
# long list, one line per entry
|
||||||
|
lines = ["[", level + "]"]
|
||||||
|
lines[1:1] = [leadin + pformat_value(x, indent + 1) + "," for x in value]
|
||||||
|
return "\n".join(lines)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
lines = ["{"]
|
||||||
|
for key, val in sorted(value.items()):
|
||||||
|
if val is None or val == []:
|
||||||
|
continue
|
||||||
|
lines.append(leadin + key + ": " + pformat_value(val, indent + 1) + ",")
|
||||||
|
lines.append(level + "}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
if isinstance(value, (bytes, bytearray)):
|
||||||
|
length = len(value)
|
||||||
|
suffix = ""
|
||||||
|
if truncate_after and length > truncate_after:
|
||||||
|
suffix = "..."
|
||||||
|
value = value[: truncate_to or 0]
|
||||||
|
if mostly_printable(value):
|
||||||
|
output = repr(value)
|
||||||
|
else:
|
||||||
|
output = "0x" + value.hex()
|
||||||
|
return "{} bytes {}{}".format(length, output, suffix)
|
||||||
|
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
return "{name} ({size} bytes) {content}".format(
|
||||||
|
name=pb.__class__.__name__,
|
||||||
|
size=pb.ByteSize(),
|
||||||
|
content=pformat_value(pb.__dict__, indent),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def value_to_proto(ftype, value):
|
||||||
|
if issubclass(ftype, MessageType):
|
||||||
|
raise TypeError("value_to_proto only converts simple values")
|
||||||
|
|
||||||
|
if ftype in (UVarintType, SVarintType):
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
if ftype is BoolType:
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
if ftype is UnicodeType:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
if ftype is BytesType:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return bytes.fromhex(value)
|
||||||
|
elif isinstance(value, bytes):
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise TypeError("can't convert {} value to bytes".format(type(value)))
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_proto(message_type, d):
|
||||||
|
params = {}
|
||||||
|
for fname, ftype, fflags in message_type.get_fields().values():
|
||||||
|
repeated = fflags & FLAG_REPEATED
|
||||||
|
value = d.get(fname)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not repeated:
|
||||||
|
value = [value]
|
||||||
|
|
||||||
|
if issubclass(ftype, MessageType):
|
||||||
|
function = dict_to_proto
|
||||||
|
else:
|
||||||
|
function = value_to_proto
|
||||||
|
|
||||||
|
newvalue = [function(ftype, v) for v in value]
|
||||||
|
|
||||||
|
if not repeated:
|
||||||
|
newvalue = newvalue[0]
|
||||||
|
|
||||||
|
params[fname] = newvalue
|
||||||
|
return message_type(**params)
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(msg, hexlify_bytes=True):
|
||||||
|
def convert_value(value):
|
||||||
|
if hexlify_bytes and isinstance(value, bytes):
|
||||||
|
return value.hex()
|
||||||
|
elif isinstance(value, MessageType):
|
||||||
|
return to_dict(value, hexlify_bytes)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return [convert_value(v) for v in value]
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
for key, value in msg.__dict__.items():
|
||||||
|
if value is None or value == []:
|
||||||
|
continue
|
||||||
|
res[key] = convert_value(value)
|
||||||
|
|
||||||
|
return res
|
0
python/trezorlib/qt/__init__.py
Normal file
0
python/trezorlib/qt/__init__.py
Normal file
173
python/trezorlib/qt/pinmatrix.py
Normal file
173
python/trezorlib/qt/pinmatrix.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QPushButton,
|
||||||
|
QLineEdit,
|
||||||
|
QSizePolicy,
|
||||||
|
QLabel,
|
||||||
|
QApplication,
|
||||||
|
QWidget,
|
||||||
|
QGridLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
)
|
||||||
|
from PyQt5.QtGui import QRegExpValidator
|
||||||
|
from PyQt5.QtCore import QRegExp, Qt, QT_VERSION_STR
|
||||||
|
except Exception:
|
||||||
|
from PyQt4.QtGui import (
|
||||||
|
QPushButton,
|
||||||
|
QLineEdit,
|
||||||
|
QSizePolicy,
|
||||||
|
QRegExpValidator,
|
||||||
|
QLabel,
|
||||||
|
QApplication,
|
||||||
|
QWidget,
|
||||||
|
QGridLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
)
|
||||||
|
from PyQt4.QtCore import QObject, SIGNAL, QRegExp, Qt, QT_VERSION_STR
|
||||||
|
|
||||||
|
|
||||||
|
class PinButton(QPushButton):
|
||||||
|
def __init__(self, password, encoded_value):
|
||||||
|
super(PinButton, self).__init__("?")
|
||||||
|
self.password = password
|
||||||
|
self.encoded_value = encoded_value
|
||||||
|
|
||||||
|
if QT_VERSION_STR >= "5":
|
||||||
|
self.clicked.connect(self._pressed)
|
||||||
|
elif QT_VERSION_STR >= "4":
|
||||||
|
QObject.connect(self, SIGNAL("clicked()"), self._pressed)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unsupported Qt version")
|
||||||
|
|
||||||
|
def _pressed(self):
|
||||||
|
self.password.setText(self.password.text() + str(self.encoded_value))
|
||||||
|
self.password.setFocus()
|
||||||
|
|
||||||
|
|
||||||
|
class PinMatrixWidget(QWidget):
|
||||||
|
"""
|
||||||
|
Displays widget with nine blank buttons and password box.
|
||||||
|
Encodes button clicks into sequence of numbers for passing
|
||||||
|
into PinAck messages of TREZOR.
|
||||||
|
|
||||||
|
show_strength=True may be useful for entering new PIN
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, show_strength=True, parent=None):
|
||||||
|
super(PinMatrixWidget, self).__init__(parent)
|
||||||
|
|
||||||
|
self.password = QLineEdit()
|
||||||
|
self.password.setValidator(QRegExpValidator(QRegExp("[1-9]+"), None))
|
||||||
|
self.password.setEchoMode(QLineEdit.Password)
|
||||||
|
|
||||||
|
if QT_VERSION_STR >= "5":
|
||||||
|
self.password.textChanged.connect(self._password_changed)
|
||||||
|
elif QT_VERSION_STR >= "4":
|
||||||
|
QObject.connect(
|
||||||
|
self.password, SIGNAL("textChanged(QString)"), self._password_changed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unsupported Qt version")
|
||||||
|
|
||||||
|
self.strength = QLabel()
|
||||||
|
self.strength.setMinimumWidth(75)
|
||||||
|
self.strength.setAlignment(Qt.AlignCenter)
|
||||||
|
self._set_strength(0)
|
||||||
|
|
||||||
|
grid = QGridLayout()
|
||||||
|
grid.setSpacing(0)
|
||||||
|
for y in range(3)[::-1]:
|
||||||
|
for x in range(3):
|
||||||
|
button = PinButton(self.password, x + y * 3 + 1)
|
||||||
|
button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
button.setFocusPolicy(Qt.NoFocus)
|
||||||
|
grid.addWidget(button, 3 - y, x)
|
||||||
|
|
||||||
|
hbox = QHBoxLayout()
|
||||||
|
hbox.addWidget(self.password)
|
||||||
|
if show_strength:
|
||||||
|
hbox.addWidget(self.strength)
|
||||||
|
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
vbox.addLayout(grid)
|
||||||
|
vbox.addLayout(hbox)
|
||||||
|
self.setLayout(vbox)
|
||||||
|
|
||||||
|
def _set_strength(self, strength):
|
||||||
|
if strength < 3000:
|
||||||
|
self.strength.setText("weak")
|
||||||
|
self.strength.setStyleSheet("QLabel { color : #d00; }")
|
||||||
|
elif strength < 60000:
|
||||||
|
self.strength.setText("fine")
|
||||||
|
self.strength.setStyleSheet("QLabel { color : #db0; }")
|
||||||
|
elif strength < 360000:
|
||||||
|
self.strength.setText("strong")
|
||||||
|
self.strength.setStyleSheet("QLabel { color : #0a0; }")
|
||||||
|
else:
|
||||||
|
self.strength.setText("ULTIMATE")
|
||||||
|
self.strength.setStyleSheet("QLabel { color : #000; font-weight: bold;}")
|
||||||
|
|
||||||
|
def _password_changed(self, password):
|
||||||
|
self._set_strength(self.get_strength())
|
||||||
|
|
||||||
|
def get_strength(self):
|
||||||
|
digits = len(set(str(self.password.text())))
|
||||||
|
strength = math.factorial(9) / math.factorial(9 - digits)
|
||||||
|
return strength
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self.password.text()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Demo application showing PinMatrix widget in action
|
||||||
|
"""
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
matrix = PinMatrixWidget()
|
||||||
|
|
||||||
|
def clicked():
|
||||||
|
print("PinMatrix value is", matrix.get_value())
|
||||||
|
print("Possible button combinations:", matrix.get_strength())
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
ok = QPushButton("OK")
|
||||||
|
if QT_VERSION_STR >= "5":
|
||||||
|
ok.clicked.connect(clicked)
|
||||||
|
elif QT_VERSION_STR >= "4":
|
||||||
|
QObject.connect(ok, SIGNAL("clicked()"), clicked)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unsupported Qt version")
|
||||||
|
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
vbox.addWidget(matrix)
|
||||||
|
vbox.addWidget(ok)
|
||||||
|
|
||||||
|
w = QWidget()
|
||||||
|
w.setLayout(vbox)
|
||||||
|
w.move(100, 100)
|
||||||
|
w.show()
|
||||||
|
|
||||||
|
app.exec_()
|
47
python/trezorlib/ripple.py
Normal file
47
python/trezorlib/ripple.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from . import messages
|
||||||
|
from .protobuf import dict_to_proto
|
||||||
|
from .tools import dict_from_camelcase, expect
|
||||||
|
|
||||||
|
REQUIRED_FIELDS = ("Fee", "Sequence", "TransactionType", "Payment")
|
||||||
|
REQUIRED_PAYMENT_FIELDS = ("Amount", "Destination")
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.RippleAddress, field="address")
|
||||||
|
def get_address(client, address_n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
messages.RippleGetAddress(address_n=address_n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.RippleSignedTx)
|
||||||
|
def sign_tx(client, address_n, msg: messages.RippleSignTx):
|
||||||
|
msg.address_n = address_n
|
||||||
|
return client.call(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sign_tx_msg(transaction) -> messages.RippleSignTx:
|
||||||
|
if not all(transaction.get(k) for k in REQUIRED_FIELDS):
|
||||||
|
raise ValueError("Some of the required fields missing")
|
||||||
|
if not all(transaction["Payment"].get(k) for k in REQUIRED_PAYMENT_FIELDS):
|
||||||
|
raise ValueError("Some of the required payment fields missing")
|
||||||
|
if transaction["TransactionType"] != "Payment":
|
||||||
|
raise ValueError("Only Payment transaction type is supported")
|
||||||
|
|
||||||
|
converted = dict_from_camelcase(transaction)
|
||||||
|
return dict_to_proto(messages.RippleSignTx, converted)
|
385
python/trezorlib/stellar.py
Normal file
385
python/trezorlib/stellar.py
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import struct
|
||||||
|
import xdrlib
|
||||||
|
|
||||||
|
from . import messages
|
||||||
|
from .tools import CallException, expect
|
||||||
|
|
||||||
|
# Memo types
|
||||||
|
MEMO_TYPE_NONE = 0
|
||||||
|
MEMO_TYPE_TEXT = 1
|
||||||
|
MEMO_TYPE_ID = 2
|
||||||
|
MEMO_TYPE_HASH = 3
|
||||||
|
MEMO_TYPE_RETURN = 4
|
||||||
|
|
||||||
|
# Asset types
|
||||||
|
ASSET_TYPE_NATIVE = 0
|
||||||
|
ASSET_TYPE_ALPHA4 = 1
|
||||||
|
ASSET_TYPE_ALPHA12 = 2
|
||||||
|
|
||||||
|
# Operations
|
||||||
|
OP_CREATE_ACCOUNT = 0
|
||||||
|
OP_PAYMENT = 1
|
||||||
|
OP_PATH_PAYMENT = 2
|
||||||
|
OP_MANAGE_OFFER = 3
|
||||||
|
OP_CREATE_PASSIVE_OFFER = 4
|
||||||
|
OP_SET_OPTIONS = 5
|
||||||
|
OP_CHANGE_TRUST = 6
|
||||||
|
OP_ALLOW_TRUST = 7
|
||||||
|
OP_ACCOUNT_MERGE = 8
|
||||||
|
OP_INFLATION = 9 # Included for documentation purposes, not supported by Trezor
|
||||||
|
OP_MANAGE_DATA = 10
|
||||||
|
OP_BUMP_SEQUENCE = 11
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_BIP32_PATH = "m/44h/148h/0h"
|
||||||
|
# Stellar's BIP32 differs to Bitcoin's see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md
|
||||||
|
DEFAULT_NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015"
|
||||||
|
|
||||||
|
|
||||||
|
def address_from_public_key(pk_bytes):
|
||||||
|
"""Returns the base32-encoded version of pk_bytes (G...)
|
||||||
|
"""
|
||||||
|
final_bytes = bytearray()
|
||||||
|
|
||||||
|
# version
|
||||||
|
final_bytes.append(6 << 3)
|
||||||
|
# public key
|
||||||
|
final_bytes.extend(pk_bytes)
|
||||||
|
# checksum
|
||||||
|
final_bytes.extend(struct.pack("<H", _crc16_checksum(final_bytes)))
|
||||||
|
|
||||||
|
return base64.b32encode(final_bytes).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def address_to_public_key(address_str):
|
||||||
|
"""Returns the raw 32 bytes representing a public key by extracting
|
||||||
|
it from the G... string
|
||||||
|
"""
|
||||||
|
decoded = base64.b32decode(address_str)
|
||||||
|
|
||||||
|
# skip 0th byte (version) and last two bytes (checksum)
|
||||||
|
return decoded[1:-2]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_transaction_bytes(tx_bytes):
|
||||||
|
"""Parses base64data into a map with the following keys:
|
||||||
|
tx - a StellarSignTx describing the transaction header
|
||||||
|
operations - an array of protobuf message objects for each operation
|
||||||
|
"""
|
||||||
|
tx = messages.StellarSignTx()
|
||||||
|
unpacker = xdrlib.Unpacker(tx_bytes)
|
||||||
|
|
||||||
|
tx.source_account = _xdr_read_address(unpacker)
|
||||||
|
tx.fee = unpacker.unpack_uint()
|
||||||
|
tx.sequence_number = unpacker.unpack_uhyper()
|
||||||
|
|
||||||
|
# Timebounds is an optional field
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
max_timebound = 2 ** 32 - 1 # max unsigned 32-bit int
|
||||||
|
# (trezor does not support the full 64-bit time value)
|
||||||
|
|
||||||
|
tx.timebounds_start = unpacker.unpack_uhyper()
|
||||||
|
tx.timebounds_end = unpacker.unpack_uhyper()
|
||||||
|
|
||||||
|
if tx.timebounds_start > max_timebound or tx.timebounds_start < 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Starting timebound out of range (must be between 0 and "
|
||||||
|
+ max_timebound
|
||||||
|
)
|
||||||
|
if tx.timebounds_end > max_timebound or tx.timebounds_end < 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Ending timebound out of range (must be between 0 and " + max_timebound
|
||||||
|
)
|
||||||
|
|
||||||
|
# memo type determines what optional fields are set
|
||||||
|
tx.memo_type = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# text
|
||||||
|
if tx.memo_type == MEMO_TYPE_TEXT:
|
||||||
|
tx.memo_text = unpacker.unpack_string()
|
||||||
|
# id (64-bit uint)
|
||||||
|
if tx.memo_type == MEMO_TYPE_ID:
|
||||||
|
tx.memo_id = unpacker.unpack_uhyper()
|
||||||
|
# hash / return are the same structure (32 bytes representing a hash)
|
||||||
|
if tx.memo_type == MEMO_TYPE_HASH or tx.memo_type == MEMO_TYPE_RETURN:
|
||||||
|
tx.memo_hash = unpacker.unpack_fopaque(32)
|
||||||
|
|
||||||
|
tx.num_operations = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
for _ in range(tx.num_operations):
|
||||||
|
operations.append(_parse_operation_bytes(unpacker))
|
||||||
|
|
||||||
|
return tx, operations
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_operation_bytes(unpacker):
|
||||||
|
"""Returns a protobuf message representing the next operation as read from
|
||||||
|
the byte stream in unpacker
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check for and parse optional source account field
|
||||||
|
source_account = None
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
source_account = unpacker.unpack_fopaque(32)
|
||||||
|
|
||||||
|
# Operation type (See OP_ constants)
|
||||||
|
type = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
if type == OP_CREATE_ACCOUNT:
|
||||||
|
return messages.StellarCreateAccountOp(
|
||||||
|
source_account=source_account,
|
||||||
|
new_account=_xdr_read_address(unpacker),
|
||||||
|
starting_balance=unpacker.unpack_hyper(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == OP_PAYMENT:
|
||||||
|
return messages.StellarPaymentOp(
|
||||||
|
source_account=source_account,
|
||||||
|
destination_account=_xdr_read_address(unpacker),
|
||||||
|
asset=_xdr_read_asset(unpacker),
|
||||||
|
amount=unpacker.unpack_hyper(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == OP_PATH_PAYMENT:
|
||||||
|
op = messages.StellarPathPaymentOp(
|
||||||
|
source_account=source_account,
|
||||||
|
send_asset=_xdr_read_asset(unpacker),
|
||||||
|
send_max=unpacker.unpack_hyper(),
|
||||||
|
destination_account=_xdr_read_address(unpacker),
|
||||||
|
destination_asset=_xdr_read_asset(unpacker),
|
||||||
|
destination_amount=unpacker.unpack_hyper(),
|
||||||
|
paths=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
num_paths = unpacker.unpack_uint()
|
||||||
|
for _ in range(num_paths):
|
||||||
|
op.paths.append(_xdr_read_asset(unpacker))
|
||||||
|
|
||||||
|
return op
|
||||||
|
|
||||||
|
if type == OP_MANAGE_OFFER:
|
||||||
|
return messages.StellarManageOfferOp(
|
||||||
|
source_account=source_account,
|
||||||
|
selling_asset=_xdr_read_asset(unpacker),
|
||||||
|
buying_asset=_xdr_read_asset(unpacker),
|
||||||
|
amount=unpacker.unpack_hyper(),
|
||||||
|
price_n=unpacker.unpack_uint(),
|
||||||
|
price_d=unpacker.unpack_uint(),
|
||||||
|
offer_id=unpacker.unpack_uhyper(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == OP_CREATE_PASSIVE_OFFER:
|
||||||
|
return messages.StellarCreatePassiveOfferOp(
|
||||||
|
source_account=source_account,
|
||||||
|
selling_asset=_xdr_read_asset(unpacker),
|
||||||
|
buying_asset=_xdr_read_asset(unpacker),
|
||||||
|
amount=unpacker.unpack_hyper(),
|
||||||
|
price_n=unpacker.unpack_uint(),
|
||||||
|
price_d=unpacker.unpack_uint(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == OP_SET_OPTIONS:
|
||||||
|
op = messages.StellarSetOptionsOp(source_account=source_account)
|
||||||
|
|
||||||
|
# Inflation destination
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.inflation_destination_account = _xdr_read_address(unpacker)
|
||||||
|
|
||||||
|
# clear flags
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.clear_flags = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# set flags
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.set_flags = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# master weight
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.master_weight = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# low threshold
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.low_threshold = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# medium threshold
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.medium_threshold = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# high threshold
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.high_threshold = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
# home domain
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.home_domain = unpacker.unpack_string()
|
||||||
|
|
||||||
|
# signer
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.signer_type = unpacker.unpack_uint()
|
||||||
|
op.signer_key = unpacker.unpack_fopaque(32)
|
||||||
|
op.signer_weight = unpacker.unpack_uint()
|
||||||
|
|
||||||
|
return op
|
||||||
|
|
||||||
|
if type == OP_CHANGE_TRUST:
|
||||||
|
return messages.StellarChangeTrustOp(
|
||||||
|
source_account=source_account,
|
||||||
|
asset=_xdr_read_asset(unpacker),
|
||||||
|
limit=unpacker.unpack_uhyper(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == OP_ALLOW_TRUST:
|
||||||
|
op = messages.StellarAllowTrustOp(
|
||||||
|
source_account=source_account,
|
||||||
|
trusted_account=_xdr_read_address(unpacker),
|
||||||
|
asset_type=unpacker.unpack_uint(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if op.asset_type == ASSET_TYPE_ALPHA4:
|
||||||
|
op.asset_code = unpacker.unpack_fstring(4)
|
||||||
|
if op.asset_type == ASSET_TYPE_ALPHA12:
|
||||||
|
op.asset_code = unpacker.unpack_fstring(12)
|
||||||
|
|
||||||
|
op.is_authorized = unpacker.unpack_bool()
|
||||||
|
|
||||||
|
return op
|
||||||
|
|
||||||
|
if type == OP_ACCOUNT_MERGE:
|
||||||
|
return messages.StellarAccountMergeOp(
|
||||||
|
source_account=source_account,
|
||||||
|
destination_account=_xdr_read_address(unpacker),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inflation is not implemented since anyone can submit this operation to the network
|
||||||
|
|
||||||
|
if type == OP_MANAGE_DATA:
|
||||||
|
op = messages.StellarManageDataOp(
|
||||||
|
source_account=source_account, key=unpacker.unpack_string()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only set value if the field is present
|
||||||
|
if unpacker.unpack_bool():
|
||||||
|
op.value = unpacker.unpack_opaque()
|
||||||
|
|
||||||
|
return op
|
||||||
|
|
||||||
|
# Bump Sequence
|
||||||
|
# see: https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x#L269
|
||||||
|
if type == OP_BUMP_SEQUENCE:
|
||||||
|
return messages.StellarBumpSequenceOp(
|
||||||
|
source_account=source_account, bump_to=unpacker.unpack_uhyper()
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError("Unknown operation type: " + str(type))
|
||||||
|
|
||||||
|
|
||||||
|
def _xdr_read_asset(unpacker):
|
||||||
|
"""Reads a stellar Asset from unpacker"""
|
||||||
|
asset = messages.StellarAssetType(type=unpacker.unpack_uint())
|
||||||
|
|
||||||
|
if asset.type == ASSET_TYPE_ALPHA4:
|
||||||
|
asset.code = unpacker.unpack_fstring(4)
|
||||||
|
asset.issuer = _xdr_read_address(unpacker)
|
||||||
|
|
||||||
|
if asset.type == ASSET_TYPE_ALPHA12:
|
||||||
|
asset.code = unpacker.unpack_fstring(12)
|
||||||
|
asset.issuer = _xdr_read_address(unpacker)
|
||||||
|
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def _xdr_read_address(unpacker):
|
||||||
|
"""Reads a stellar address and returns the string representing the address
|
||||||
|
This method assumes the encoded address is a public address (starting with G)
|
||||||
|
"""
|
||||||
|
# First 4 bytes are the address type
|
||||||
|
address_type = unpacker.unpack_uint()
|
||||||
|
if address_type != 0:
|
||||||
|
raise ValueError("Unsupported address type")
|
||||||
|
|
||||||
|
return address_from_public_key(unpacker.unpack_fopaque(32))
|
||||||
|
|
||||||
|
|
||||||
|
def _crc16_checksum(bytes):
|
||||||
|
"""Returns the CRC-16 checksum of bytearray bytes
|
||||||
|
|
||||||
|
Ported from Java implementation at: http://introcs.cs.princeton.edu/java/61data/CRC16CCITT.java.html
|
||||||
|
|
||||||
|
Initial value changed to 0x0000 to match Stellar configuration.
|
||||||
|
"""
|
||||||
|
crc = 0x0000
|
||||||
|
polynomial = 0x1021
|
||||||
|
|
||||||
|
for byte in bytes:
|
||||||
|
for i in range(8):
|
||||||
|
bit = (byte >> (7 - i) & 1) == 1
|
||||||
|
c15 = (crc >> 15 & 1) == 1
|
||||||
|
crc <<= 1
|
||||||
|
if c15 ^ bit:
|
||||||
|
crc ^= polynomial
|
||||||
|
|
||||||
|
return crc & 0xFFFF
|
||||||
|
|
||||||
|
|
||||||
|
# ====== Client functions ====== #
|
||||||
|
|
||||||
|
|
||||||
|
@expect(messages.StellarAddress, field="address")
|
||||||
|
def get_address(client, address_n, show_display=False):
|
||||||
|
return client.call(
|
||||||
|
messages.StellarGetAddress(address_n=address_n, show_display=show_display)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_tx(
|
||||||
|
client, tx, operations, address_n, network_passphrase=DEFAULT_NETWORK_PASSPHRASE
|
||||||
|
):
|
||||||
|
tx.network_passphrase = network_passphrase
|
||||||
|
tx.address_n = address_n
|
||||||
|
tx.num_operations = len(operations)
|
||||||
|
# Signing loop works as follows:
|
||||||
|
#
|
||||||
|
# 1. Start with tx (header information for the transaction) and operations (an array of operation protobuf messagess)
|
||||||
|
# 2. Send the tx header to the device
|
||||||
|
# 3. Receive a StellarTxOpRequest message
|
||||||
|
# 4. Send operations one by one until all operations have been sent. If there are more operations to sign, the device will send a StellarTxOpRequest message
|
||||||
|
# 5. The final message received will be StellarSignedTx which is returned from this method
|
||||||
|
resp = client.call(tx)
|
||||||
|
try:
|
||||||
|
while isinstance(resp, messages.StellarTxOpRequest):
|
||||||
|
resp = client.call(operations.pop(0))
|
||||||
|
except IndexError:
|
||||||
|
# pop from empty list
|
||||||
|
raise CallException(
|
||||||
|
"Stellar.UnexpectedEndOfOperations",
|
||||||
|
"Reached end of operations without a signature.",
|
||||||
|
) from None
|
||||||
|
|
||||||
|
if not isinstance(resp, messages.StellarSignedTx):
|
||||||
|
raise CallException(messages.FailureType.UnexpectedMessage, resp)
|
||||||
|
|
||||||
|
if operations:
|
||||||
|
raise CallException(
|
||||||
|
"Stellar.UnprocessedOperations",
|
||||||
|
"Received a signature before processing all operations.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return resp
|
0
python/trezorlib/tests/__init__.py
Normal file
0
python/trezorlib/tests/__init__.py
Normal file
75
python/trezorlib/tests/burn_tests/burntest_t1.py
Executable file
75
python/trezorlib/tests/burn_tests/burntest_t1.py
Executable file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from trezorlib import device
|
||||||
|
from trezorlib.debuglink import TrezorClientDebugLink
|
||||||
|
from trezorlib.transport import enumerate_devices, get_transport
|
||||||
|
|
||||||
|
|
||||||
|
def get_device():
|
||||||
|
path = os.environ.get("TREZOR_PATH")
|
||||||
|
if path:
|
||||||
|
return get_transport(path)
|
||||||
|
else:
|
||||||
|
devices = enumerate_devices()
|
||||||
|
for d in devices:
|
||||||
|
if hasattr(d, "find_debug"):
|
||||||
|
return d
|
||||||
|
raise RuntimeError("No debuggable device found")
|
||||||
|
|
||||||
|
|
||||||
|
def pin_input_flow(client, old_pin, new_pin):
|
||||||
|
# do you want to change pin?
|
||||||
|
yield
|
||||||
|
client.debug.press_yes()
|
||||||
|
if old_pin is not None:
|
||||||
|
# enter old pin
|
||||||
|
yield
|
||||||
|
client.debug.input(old_pin)
|
||||||
|
# enter new pin
|
||||||
|
yield
|
||||||
|
client.debug.input(new_pin)
|
||||||
|
# repeat new pin
|
||||||
|
yield
|
||||||
|
client.debug.input(new_pin)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
wirelink = get_device()
|
||||||
|
client = TrezorClientDebugLink(wirelink)
|
||||||
|
client.open()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
last_pin = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# set private field
|
||||||
|
device.apply_settings(client, auto_lock_delay_ms=(i % 10 + 10) * 1000)
|
||||||
|
|
||||||
|
# set public field
|
||||||
|
label = "".join(random.choices(string.ascii_uppercase + string.digits, k=17))
|
||||||
|
device.apply_settings(client, label=label)
|
||||||
|
assert client.features.label == label
|
||||||
|
|
||||||
|
print("iteration %d" % i)
|
||||||
|
i = i + 1
|
85
python/trezorlib/tests/burn_tests/burntest_t2.py
Executable file
85
python/trezorlib/tests/burn_tests/burntest_t2.py
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from trezorlib import device
|
||||||
|
from trezorlib.debuglink import TrezorClientDebugLink
|
||||||
|
from trezorlib.transport import enumerate_devices, get_transport
|
||||||
|
|
||||||
|
|
||||||
|
def get_device():
|
||||||
|
path = os.environ.get("TREZOR_PATH")
|
||||||
|
if path:
|
||||||
|
return get_transport(path)
|
||||||
|
else:
|
||||||
|
devices = enumerate_devices()
|
||||||
|
for d in devices:
|
||||||
|
if hasattr(d, "find_debug"):
|
||||||
|
return d
|
||||||
|
raise RuntimeError("No debuggable device found")
|
||||||
|
|
||||||
|
|
||||||
|
def pin_input_flow(client, old_pin, new_pin):
|
||||||
|
# do you want to change pin?
|
||||||
|
yield
|
||||||
|
client.debug.press_yes()
|
||||||
|
if old_pin is not None:
|
||||||
|
# enter old pin
|
||||||
|
yield
|
||||||
|
client.debug.input(old_pin)
|
||||||
|
# enter new pin
|
||||||
|
yield
|
||||||
|
client.debug.input(new_pin)
|
||||||
|
# repeat new pin
|
||||||
|
yield
|
||||||
|
client.debug.input(new_pin)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
wirelink = get_device()
|
||||||
|
client = TrezorClientDebugLink(wirelink)
|
||||||
|
client.open()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
last_pin = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# set private field
|
||||||
|
device.apply_settings(client, use_passphrase=True)
|
||||||
|
assert client.features.passphrase_protection is True
|
||||||
|
device.apply_settings(client, use_passphrase=False)
|
||||||
|
assert client.features.passphrase_protection is False
|
||||||
|
|
||||||
|
# set public field
|
||||||
|
label = "".join(random.choices(string.ascii_uppercase + string.digits, k=17))
|
||||||
|
device.apply_settings(client, label=label)
|
||||||
|
assert client.features.label == label
|
||||||
|
|
||||||
|
# change PIN
|
||||||
|
new_pin = "".join(random.choices(string.digits, k=random.randint(6, 10)))
|
||||||
|
client.set_input_flow(pin_input_flow(client, last_pin, new_pin))
|
||||||
|
device.change_pin(client)
|
||||||
|
client.set_input_flow(None)
|
||||||
|
last_pin = new_pin
|
||||||
|
|
||||||
|
print("iteration %d" % i)
|
||||||
|
i = i + 1
|
2
python/trezorlib/tests/device_tests/.gitignore
vendored
Normal file
2
python/trezorlib/tests/device_tests/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.out
|
||||||
|
*.err
|
0
python/trezorlib/tests/device_tests/__init__.py
Normal file
0
python/trezorlib/tests/device_tests/__init__.py
Normal file
107
python/trezorlib/tests/device_tests/common.py
Normal file
107
python/trezorlib/tests/device_tests/common.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import debuglink, device
|
||||||
|
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||||
|
|
||||||
|
from . import conftest
|
||||||
|
|
||||||
|
|
||||||
|
class TrezorTest:
|
||||||
|
# fmt: off
|
||||||
|
# 1 2 3 4 5 6 7 8 9 10 11 12
|
||||||
|
mnemonic12 = "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
|
||||||
|
mnemonic18 = "owner little vague addict embark decide pink prosper true fork panda embody mixture exchange choose canoe electric jewel"
|
||||||
|
mnemonic24 = "dignity pass list indicate nasty swamp pool script soccer toe leaf photo multiply desk host tomato cradle drill spread actor shine dismiss champion exotic"
|
||||||
|
mnemonic_all = " ".join(["all"] * 12)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
pin4 = "1234"
|
||||||
|
pin6 = "789456"
|
||||||
|
pin8 = "45678978"
|
||||||
|
|
||||||
|
def setup_method(self, method):
|
||||||
|
self.client = conftest.get_device()
|
||||||
|
# self.client.set_buttonwait(3)
|
||||||
|
|
||||||
|
device.wipe(self.client)
|
||||||
|
self.client.open()
|
||||||
|
|
||||||
|
def teardown_method(self, method):
|
||||||
|
self.client.close()
|
||||||
|
|
||||||
|
def _setup_mnemonic(self, mnemonic=None, pin="", passphrase=False, lock=True):
|
||||||
|
if mnemonic is None:
|
||||||
|
mnemonic = TrezorTest.mnemonic12
|
||||||
|
debuglink.load_device_by_mnemonic(
|
||||||
|
self.client,
|
||||||
|
mnemonic=mnemonic,
|
||||||
|
pin=pin,
|
||||||
|
passphrase_protection=passphrase,
|
||||||
|
label="test",
|
||||||
|
language="english",
|
||||||
|
)
|
||||||
|
if conftest.TREZOR_VERSION == 1 and lock:
|
||||||
|
# remove cached PIN (introduced via load_device)
|
||||||
|
self.client.clear_session()
|
||||||
|
if conftest.TREZOR_VERSION > 1 and passphrase:
|
||||||
|
device.apply_settings(self.client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||||
|
|
||||||
|
def setup_mnemonic_allallall(self, lock=True):
|
||||||
|
self._setup_mnemonic(mnemonic=TrezorTest.mnemonic_all, lock=lock)
|
||||||
|
|
||||||
|
def setup_mnemonic_nopin_nopassphrase(self, lock=True):
|
||||||
|
self._setup_mnemonic(lock=lock)
|
||||||
|
|
||||||
|
def setup_mnemonic_nopin_passphrase(self, lock=True):
|
||||||
|
self._setup_mnemonic(passphrase=True, lock=lock)
|
||||||
|
|
||||||
|
def setup_mnemonic_pin_nopassphrase(self, lock=True):
|
||||||
|
self._setup_mnemonic(pin=TrezorTest.pin4, lock=lock)
|
||||||
|
|
||||||
|
def setup_mnemonic_pin_passphrase(self, lock=True):
|
||||||
|
self._setup_mnemonic(pin=TrezorTest.pin4, passphrase=True, lock=lock)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_entropy(strength, internal_entropy, external_entropy):
|
||||||
|
"""
|
||||||
|
strength - length of produced seed. One of 128, 192, 256
|
||||||
|
random - binary stream of random data from external HRNG
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
if strength not in (128, 192, 256):
|
||||||
|
raise ValueError("Invalid strength")
|
||||||
|
|
||||||
|
if not internal_entropy:
|
||||||
|
raise ValueError("Internal entropy is not provided")
|
||||||
|
|
||||||
|
if len(internal_entropy) < 32:
|
||||||
|
raise ValueError("Internal entropy too short")
|
||||||
|
|
||||||
|
if not external_entropy:
|
||||||
|
raise ValueError("External entropy is not provided")
|
||||||
|
|
||||||
|
if len(external_entropy) < 32:
|
||||||
|
raise ValueError("External entropy too short")
|
||||||
|
|
||||||
|
entropy = hashlib.sha256(internal_entropy + external_entropy).digest()
|
||||||
|
entropy_stripped = entropy[: strength // 8]
|
||||||
|
|
||||||
|
if len(entropy_stripped) * 8 != strength:
|
||||||
|
raise ValueError("Entropy length mismatch")
|
||||||
|
|
||||||
|
return entropy_stripped
|
140
python/trezorlib/tests/device_tests/conftest.py
Normal file
140
python/trezorlib/tests/device_tests/conftest.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import debuglink, log
|
||||||
|
from trezorlib.debuglink import TrezorClientDebugLink
|
||||||
|
from trezorlib.device import wipe as wipe_device
|
||||||
|
from trezorlib.transport import enumerate_devices, get_transport
|
||||||
|
|
||||||
|
TREZOR_VERSION = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_device():
|
||||||
|
path = os.environ.get("TREZOR_PATH")
|
||||||
|
if path:
|
||||||
|
transport = get_transport(path)
|
||||||
|
else:
|
||||||
|
devices = enumerate_devices()
|
||||||
|
for device in devices:
|
||||||
|
if hasattr(device, "find_debug"):
|
||||||
|
transport = device
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No debuggable device found")
|
||||||
|
env_interactive = int(os.environ.get("INTERACT", 0))
|
||||||
|
try:
|
||||||
|
return TrezorClientDebugLink(transport, auto_interact=not env_interactive)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to open debuglink for {}".format(transport.get_path())
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def device_version():
|
||||||
|
client = get_device()
|
||||||
|
if client.features.model == "T":
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def client():
|
||||||
|
client = get_device()
|
||||||
|
wipe_device(client)
|
||||||
|
|
||||||
|
client.open()
|
||||||
|
yield client
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_client(mnemonic=None, pin="", passphrase=False):
|
||||||
|
if mnemonic is None:
|
||||||
|
mnemonic = " ".join(["all"] * 12)
|
||||||
|
if pin is True:
|
||||||
|
pin = "1234"
|
||||||
|
|
||||||
|
def client_decorator(function):
|
||||||
|
@functools.wraps(function)
|
||||||
|
def wrapper(client, *args, **kwargs):
|
||||||
|
debuglink.load_device_by_mnemonic(
|
||||||
|
client,
|
||||||
|
mnemonic=mnemonic,
|
||||||
|
pin=pin,
|
||||||
|
passphrase_protection=passphrase,
|
||||||
|
label="test",
|
||||||
|
language="english",
|
||||||
|
)
|
||||||
|
return function(client, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return client_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
global TREZOR_VERSION
|
||||||
|
TREZOR_VERSION = device_version()
|
||||||
|
|
||||||
|
if config.getoption("verbose"):
|
||||||
|
log.enable_debug_output()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addini(
|
||||||
|
"run_xfail",
|
||||||
|
"List of markers that will run even tests that are marked as xfail",
|
||||||
|
"args",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--interactive",
|
||||||
|
action="store_true",
|
||||||
|
help="Wait for user to do interaction manually",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
"""
|
||||||
|
Called for each test item (class, individual tests).
|
||||||
|
|
||||||
|
Performs custom processing, mainly useful for trezor CI testing:
|
||||||
|
* 'skip_t2' tests are skipped on T2 and 'skip_t1' tests are skipped on T1.
|
||||||
|
* no test should have both skips at the same time
|
||||||
|
* allows to 'runxfail' tests specified by 'run_xfail' in pytest.ini
|
||||||
|
"""
|
||||||
|
if item.get_closest_marker("skip_t1") and item.get_closest_marker("skip_t2"):
|
||||||
|
pytest.fail("Don't skip tests for both trezors!")
|
||||||
|
|
||||||
|
if item.get_closest_marker("skip_t2") and TREZOR_VERSION == 2:
|
||||||
|
pytest.skip("Test excluded on Trezor T")
|
||||||
|
if item.get_closest_marker("skip_t1") and TREZOR_VERSION == 1:
|
||||||
|
pytest.skip("Test excluded on Trezor 1")
|
||||||
|
|
||||||
|
xfail = item.get_closest_marker("xfail")
|
||||||
|
runxfail_markers = item.config.getini("run_xfail")
|
||||||
|
run_xfail = any(item.get_closest_marker(marker) for marker in runxfail_markers)
|
||||||
|
if xfail and run_xfail:
|
||||||
|
# Deep hack: pytest's private _evalxfail helper determines whether the test should xfail or not.
|
||||||
|
# The helper caches its result even before this hook runs.
|
||||||
|
# Here we force-set the result to False, meaning "test does NOT xfail, run as normal"
|
||||||
|
# IOW, this is basically per-item "--runxfail"
|
||||||
|
item._evalxfail.result = False
|
49
python/trezorlib/tests/device_tests/test_basic.py
Normal file
49
python/trezorlib/tests/device_tests/test_basic.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import device, messages
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasic(TrezorTest):
|
||||||
|
def test_features(self):
|
||||||
|
f0 = self.client.features
|
||||||
|
f1 = self.client.call(messages.Initialize())
|
||||||
|
assert f0 == f1
|
||||||
|
|
||||||
|
def test_ping(self):
|
||||||
|
ping = self.client.call(messages.Ping(message="ahoj!"))
|
||||||
|
assert ping == messages.Success(message="ahoj!")
|
||||||
|
|
||||||
|
def test_device_id_same(self):
|
||||||
|
id1 = self.client.get_device_id()
|
||||||
|
self.client.init_device()
|
||||||
|
id2 = self.client.get_device_id()
|
||||||
|
|
||||||
|
# ID must be at least 12 characters
|
||||||
|
assert len(id1) >= 12
|
||||||
|
|
||||||
|
# Every resulf of UUID must be the same
|
||||||
|
assert id1 == id2
|
||||||
|
|
||||||
|
def test_device_id_different(self):
|
||||||
|
id1 = self.client.get_device_id()
|
||||||
|
device.wipe(self.client)
|
||||||
|
id2 = self.client.get_device_id()
|
||||||
|
|
||||||
|
# Device ID must be fresh after every reset
|
||||||
|
assert id1 != id2
|
73
python/trezorlib/tests/device_tests/test_bip32_speed.py
Normal file
73
python/trezorlib/tests/device_tests/test_bip32_speed.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import btc
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestBip32Speed(TrezorTest):
|
||||||
|
def test_public_ckd(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
btc.get_address(self.client, "Bitcoin", []) # to compute root node via BIP39
|
||||||
|
|
||||||
|
for depth in range(8):
|
||||||
|
start = time.time()
|
||||||
|
btc.get_address(self.client, "Bitcoin", range(depth))
|
||||||
|
delay = time.time() - start
|
||||||
|
expected = (depth + 1) * 0.26
|
||||||
|
print("DEPTH", depth, "EXPECTED DELAY", expected, "REAL DELAY", delay)
|
||||||
|
assert delay <= expected
|
||||||
|
|
||||||
|
def test_private_ckd(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
btc.get_address(self.client, "Bitcoin", []) # to compute root node via BIP39
|
||||||
|
|
||||||
|
for depth in range(8):
|
||||||
|
start = time.time()
|
||||||
|
address_n = [H_(-i) for i in range(-depth, 0)]
|
||||||
|
btc.get_address(self.client, "Bitcoin", address_n)
|
||||||
|
delay = time.time() - start
|
||||||
|
expected = (depth + 1) * 0.26
|
||||||
|
print("DEPTH", depth, "EXPECTED DELAY", expected, "REAL DELAY", delay)
|
||||||
|
assert delay <= expected
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
def test_cache(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
for x in range(10):
|
||||||
|
btc.get_address(self.client, "Bitcoin", [x, 2, 3, 4, 5, 6, 7, 8])
|
||||||
|
nocache_time = time.time() - start
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
for x in range(10):
|
||||||
|
btc.get_address(self.client, "Bitcoin", [1, 2, 3, 4, 5, 6, 7, x])
|
||||||
|
cache_time = time.time() - start
|
||||||
|
|
||||||
|
print("NOCACHE TIME", nocache_time)
|
||||||
|
print("CACHED TIME", cache_time)
|
||||||
|
|
||||||
|
# Cached time expected to be at least 2x faster
|
||||||
|
assert cache_time <= nocache_time / 2.0
|
72
python/trezorlib/tests/device_tests/test_cancel.py
Normal file
72
python/trezorlib/tests/device_tests/test_cancel.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import trezorlib.messages as m
|
||||||
|
|
||||||
|
from .conftest import setup_client
|
||||||
|
|
||||||
|
|
||||||
|
@setup_client()
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"message",
|
||||||
|
[
|
||||||
|
m.Ping(message="hello", button_protection=True),
|
||||||
|
m.GetAddress(
|
||||||
|
address_n=[0],
|
||||||
|
coin_name="Bitcoin",
|
||||||
|
script_type=m.InputScriptType.SPENDADDRESS,
|
||||||
|
show_display=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cancel_message_via_cancel(client, message):
|
||||||
|
resp = client.call_raw(message)
|
||||||
|
assert isinstance(resp, m.ButtonRequest)
|
||||||
|
|
||||||
|
client.transport.write(m.ButtonAck())
|
||||||
|
client.transport.write(m.Cancel())
|
||||||
|
|
||||||
|
resp = client.transport.read()
|
||||||
|
|
||||||
|
assert isinstance(resp, m.Failure)
|
||||||
|
assert resp.code == m.FailureType.ActionCancelled
|
||||||
|
|
||||||
|
|
||||||
|
@setup_client()
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"message",
|
||||||
|
[
|
||||||
|
m.Ping(message="hello", button_protection=True),
|
||||||
|
m.GetAddress(
|
||||||
|
address_n=[0],
|
||||||
|
coin_name="Bitcoin",
|
||||||
|
script_type=m.InputScriptType.SPENDADDRESS,
|
||||||
|
show_display=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cancel_message_via_initialize(client, message):
|
||||||
|
resp = client.call_raw(message)
|
||||||
|
assert isinstance(resp, m.ButtonRequest)
|
||||||
|
|
||||||
|
client.transport.write(m.ButtonAck())
|
||||||
|
client.transport.write(m.Initialize())
|
||||||
|
|
||||||
|
resp = client.transport.read()
|
||||||
|
|
||||||
|
assert isinstance(resp, m.Features)
|
105
python/trezorlib/tests/device_tests/test_cosi.py
Normal file
105
python/trezorlib/tests/device_tests/test_cosi.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import cosi
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
class TestCosi(TrezorTest):
|
||||||
|
def test_cosi_commit(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
digest = sha256(b"this is a message").digest()
|
||||||
|
|
||||||
|
c0 = cosi.commit(self.client, parse_path("10018'/0'"), digest)
|
||||||
|
c1 = cosi.commit(self.client, parse_path("10018'/1'"), digest)
|
||||||
|
c2 = cosi.commit(self.client, parse_path("10018'/2'"), digest)
|
||||||
|
|
||||||
|
assert c0.pubkey != c1.pubkey
|
||||||
|
assert c0.pubkey != c2.pubkey
|
||||||
|
assert c1.pubkey != c2.pubkey
|
||||||
|
|
||||||
|
assert c0.commitment != c1.commitment
|
||||||
|
assert c0.commitment != c2.commitment
|
||||||
|
assert c1.commitment != c2.commitment
|
||||||
|
|
||||||
|
digestb = sha256(b"this is a different message").digest()
|
||||||
|
|
||||||
|
c0b = cosi.commit(self.client, parse_path("10018'/0'"), digestb)
|
||||||
|
c1b = cosi.commit(self.client, parse_path("10018'/1'"), digestb)
|
||||||
|
c2b = cosi.commit(self.client, parse_path("10018'/2'"), digestb)
|
||||||
|
|
||||||
|
assert c0.pubkey == c0b.pubkey
|
||||||
|
assert c1.pubkey == c1b.pubkey
|
||||||
|
assert c2.pubkey == c2b.pubkey
|
||||||
|
|
||||||
|
assert c0.commitment != c0b.commitment
|
||||||
|
assert c1.commitment != c1b.commitment
|
||||||
|
assert c2.commitment != c2b.commitment
|
||||||
|
|
||||||
|
def test_cosi_sign(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
digest = sha256(b"this is a message").digest()
|
||||||
|
|
||||||
|
c0 = cosi.commit(self.client, parse_path("10018'/0'"), digest)
|
||||||
|
c1 = cosi.commit(self.client, parse_path("10018'/1'"), digest)
|
||||||
|
c2 = cosi.commit(self.client, parse_path("10018'/2'"), digest)
|
||||||
|
|
||||||
|
global_pk = cosi.combine_keys([c0.pubkey, c1.pubkey, c2.pubkey])
|
||||||
|
global_R = cosi.combine_keys([c0.commitment, c1.commitment, c2.commitment])
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
sig0 = cosi.sign(self.client, parse_path("10018'/0'"), digest, global_R, global_pk)
|
||||||
|
sig1 = cosi.sign(self.client, parse_path("10018'/1'"), digest, global_R, global_pk)
|
||||||
|
sig2 = cosi.sign(self.client, parse_path("10018'/2'"), digest, global_R, global_pk)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
sig = cosi.combine_sig(
|
||||||
|
global_R, [sig0.signature, sig1.signature, sig2.signature]
|
||||||
|
)
|
||||||
|
|
||||||
|
cosi.verify(sig, digest, global_pk)
|
||||||
|
|
||||||
|
def test_cosi_compat(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
digest = sha256(b"this is not a pipe").digest()
|
||||||
|
remote_commit = cosi.commit(self.client, parse_path("10018'/0'"), digest)
|
||||||
|
|
||||||
|
local_privkey = sha256(b"private key").digest()[:32]
|
||||||
|
local_pubkey = cosi.pubkey_from_privkey(local_privkey)
|
||||||
|
local_nonce, local_commitment = cosi.get_nonce(local_privkey, digest, 42)
|
||||||
|
|
||||||
|
global_pk = cosi.combine_keys([remote_commit.pubkey, local_pubkey])
|
||||||
|
global_R = cosi.combine_keys([remote_commit.commitment, local_commitment])
|
||||||
|
|
||||||
|
remote_sig = cosi.sign(
|
||||||
|
self.client, parse_path("10018'/0'"), digest, global_R, global_pk
|
||||||
|
)
|
||||||
|
local_sig = cosi.sign_with_privkey(
|
||||||
|
digest, local_privkey, global_pk, local_nonce, global_R
|
||||||
|
)
|
||||||
|
sig = cosi.combine_sig(global_R, [remote_sig.signature, local_sig])
|
||||||
|
|
||||||
|
cosi.verify(sig, digest, global_pk)
|
48
python/trezorlib/tests/device_tests/test_debuglink.py
Normal file
48
python/trezorlib/tests/device_tests/test_debuglink.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import messages as proto
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
class TestDebuglink(TrezorTest):
|
||||||
|
def test_layout(self):
|
||||||
|
layout = self.client.debug.state().layout
|
||||||
|
assert len(layout) == 1024
|
||||||
|
|
||||||
|
def test_mnemonic(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase(lock=False)
|
||||||
|
mnemonic = self.client.debug.state().mnemonic_secret
|
||||||
|
assert mnemonic == self.mnemonic12.encode()
|
||||||
|
|
||||||
|
def test_pin(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
# Manually trigger PinMatrixRequest
|
||||||
|
resp = self.client.call_raw(proto.Ping(message="test", pin_protection=True))
|
||||||
|
assert isinstance(resp, proto.PinMatrixRequest)
|
||||||
|
|
||||||
|
pin, matrix = self.client.debug.read_pin()
|
||||||
|
assert pin == "1234"
|
||||||
|
assert matrix != ""
|
||||||
|
|
||||||
|
pin_encoded = self.client.debug.read_pin_encoded()
|
||||||
|
resp = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
assert isinstance(resp, proto.Success)
|
141
python/trezorlib/tests/device_tests/test_msg_applysettings.py
Normal file
141
python/trezorlib/tests/device_tests/test_msg_applysettings.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import device, messages as proto
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
from .conftest import TREZOR_VERSION
|
||||||
|
|
||||||
|
EXPECTED_RESPONSES_NOPIN = [proto.ButtonRequest(), proto.Success(), proto.Features()]
|
||||||
|
EXPECTED_RESPONSES_PIN = [proto.PinMatrixRequest()] + EXPECTED_RESPONSES_NOPIN
|
||||||
|
|
||||||
|
if TREZOR_VERSION >= 2:
|
||||||
|
EXPECTED_RESPONSES = EXPECTED_RESPONSES_NOPIN
|
||||||
|
else:
|
||||||
|
EXPECTED_RESPONSES = EXPECTED_RESPONSES_PIN
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgApplysettings(TrezorTest):
|
||||||
|
def test_apply_settings(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
assert self.client.features.label == "test"
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES)
|
||||||
|
device.apply_settings(self.client, label="new label")
|
||||||
|
|
||||||
|
assert self.client.features.label == "new label"
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
def test_invalid_language(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
assert self.client.features.language == "english"
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES)
|
||||||
|
device.apply_settings(self.client, language="nonexistent")
|
||||||
|
|
||||||
|
assert self.client.features.language == "english"
|
||||||
|
|
||||||
|
def test_apply_settings_passphrase(self):
|
||||||
|
self.setup_mnemonic_pin_nopassphrase()
|
||||||
|
|
||||||
|
assert self.client.features.passphrase_protection is False
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES)
|
||||||
|
device.apply_settings(self.client, use_passphrase=True)
|
||||||
|
|
||||||
|
assert self.client.features.passphrase_protection is True
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES_NOPIN)
|
||||||
|
device.apply_settings(self.client, use_passphrase=False)
|
||||||
|
|
||||||
|
assert self.client.features.passphrase_protection is False
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES_NOPIN)
|
||||||
|
device.apply_settings(self.client, use_passphrase=True)
|
||||||
|
|
||||||
|
assert self.client.features.passphrase_protection is True
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
def test_apply_homescreen(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
img = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x00\x00\x00\x00\x04\x80\x00\x00\x00\x00\x00\x00\x00\x00\x04\x88\x02\x00\x00\x00\x02\x91\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x90@\x00\x11@\x00\x00\x00\x00\x00\x00\x08\x00\x10\x92\x12\x04\x00\x00\x05\x12D\x00\x00\x00\x00\x00 \x00\x00\x08\x00Q\x00\x00\x02\xc0\x00\x00\x00\x00\x00\x00\x00\x10\x02 \x01\x04J\x00)$\x00\x00\x00\x00\x80\x00\x00\x00\x00\x08\x10\xa1\x00\x00\x02\x81 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\tP\x00\x00\x00\x00\x00\x00 \x00\x00\xa0\x00\xa0R \x12\x84\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x08\x00\tP\x00\x00\x00\x00 \x00\x04 \x00\x80\x02\x00@\x02T\xc2 \x00\x00\x00\x00\x00\x00\x00\x10@\x00)\t@\n\xa0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x80@\x14\xa9H\x04\x00\x00\x88@\x00\x00\x00\x00\x00\x02\x02$\x00\x15B@\x00\nP\x00\x00\x00\x00\x00\x80\x00\x00\x91\x01UP\x00\x00 \x02\x00\x00\x00\x00\x00\x00\x02\x08@ Z\xa5 \x00\x00\x80\x00\x00\x00\x00\x00\x00\x08\xa1%\x14*\xa0\x00\x00\x02\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00@\xaa\x91 \x00\x05E\x80\x00\x00\x00\x00\x00\x02*T\x05-D\x00\x00\x05 @\x00\x00\x00\x00\x00%@\x80\x11V\xa0\x88\x00\x05@\xb0\x00\x00\x00\x00\x00\x818$\x04\xabD \x00\x06\xa1T\x00\x00\x00\x00\x02\x03\xb8\x01R\xd5\x01\x00\x00\x05AP\x00\x00\x00\x00\x08\xadT\x00\x05j\xa4@\x00\x87ah\x00\x00\x00\x00\x02\x8d\xb8\x08\x00.\x01\x00\x00\x02\xa5\xa8\x10\x00\x00\x00*\xc1\xec \n\xaa\x88 \x02@\xf6\xd0\x02\x00\x00\x00\x0bB\xb6\x14@U"\x80\x00\x01{`\x00\x00\x00\x00M\xa3\xf8 \x15*\x00\x00\x00\x10n\xc0\x04\x00\x00\x02\x06\xc2\xa8)\x00\x96\x84\x80\x00\x00\x1b\x00\x00\x80@\x10\x87\xa7\xf0\x84\x10\xaa\x10\x00\x00D\x00\x00\x02 \x00\x8a\x06\xfa\xe0P\n-\x02@\x00\x12\x00\x00\x00\x00\x10@\x83\xdf\xa0\x00\x08\xaa@\x00\x00\x01H\x00\x05H\x04\x12\x01\xf7\x81P\x02T\t\x00\x00\x00 \x00\x00\x84\x10\x00\x00z\x00@)* \x00\x00\x01\n\xa0\x02 \x05\n\x00\x00\x05\x10\x84\xa8\x84\x80\x00\x00@\x14\x00\x92\x10\x80\x00\x04\x11@\tT\x00\x00\x00\x00\n@\x00\x08\x84@$\x00H\x00\x12Q\x02\x00\x00\x00\x00\x90\x02A\x12\xa8\n\xaa\x92\x10\x04\xa8\x10@\x00\x00\x04\x04\x00\x04I\x00\x04\x14H\x80"R\x01\x00\x00\x00!@\x00\x00$\xa0EB\x80\x08\x95hH\x00\x00\x00\x84\x10 \x05Z\x00\x00(\x00\x02\x00\xa1\x01\x00\x00\x04\x00@\x82\x00\xadH*\x92P\x00\xaaP\x00\x00\x00\x00\x11\x02\x01*\xad\x01\x00\x01\x01"\x11D\x08\x00\x00\x10\x80 \x00\x81W\x80J\x94\x04\x08\xa5 !\x00\x00\x00\x02\x00B*\xae\xa1\x00\x80\x10\x01\x08\xa4\x00\x00\x00\x00\x00\x84\x00\t[@"HA\x04E\x00\x84\x00\x00\x00\x10\x00\x01J\xd5\x82\x90\x02\x00!\x02\xa2\x00\x00\x00\x00\x00\x00\x00\x05~\xa0\x00 \x10\n)\x00\x11\x00\x00\x00\x00\x00\x00!U\x80\xa8\x88\x82\x80\x01\x00\x00\x00\x00\x00\x00H@\x11\xaa\xc0\x82\x00 *\n\x00\x00\x00\x00\x00\x00\x00\x00\n\xabb@ \x04\x00! \x84\x00\x00\x00\x00\x02@\xa5\x15A$\x04\x81(\n\x00\x00\x00\x00\x00\x00 \x01\x10\x02\xe0\x91\x02\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01 \xa9\tQH@\x91 P\x00\x00\x00\x00\x00\x00\x08\x00\x00\xa0T\xa5\x00@\x80\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x00\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00 T\xa0\t\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00@\x02\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x10\x00\x00\x10\x02\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00@\x04\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x08@\x10\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES)
|
||||||
|
device.apply_settings(self.client, homescreen=img)
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
def test_apply_auto_lock_delay(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES_PIN)
|
||||||
|
device.apply_settings(self.client, auto_lock_delay_ms=int(10e3)) # 10 secs
|
||||||
|
|
||||||
|
time.sleep(0.1) # sleep less than auto-lock delay
|
||||||
|
with self.client:
|
||||||
|
# No PIN protection is required.
|
||||||
|
self.client.set_expected_responses([proto.Success()])
|
||||||
|
self.client.ping(msg="", pin_protection=True)
|
||||||
|
|
||||||
|
time.sleep(10.1) # sleep more than auto-lock delay
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[proto.PinMatrixRequest(), proto.Success()]
|
||||||
|
)
|
||||||
|
self.client.ping(msg="", pin_protection=True)
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
def test_apply_minimal_auto_lock_delay(self):
|
||||||
|
"""
|
||||||
|
Verify that the delay is not below the minimal auto-lock delay (10 secs)
|
||||||
|
otherwise the device may auto-lock before any user interaction.
|
||||||
|
"""
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(EXPECTED_RESPONSES_PIN)
|
||||||
|
# Note: the actual delay will be 10 secs (see above).
|
||||||
|
device.apply_settings(self.client, auto_lock_delay_ms=int(1e3))
|
||||||
|
|
||||||
|
time.sleep(0.1) # sleep less than auto-lock delay
|
||||||
|
with self.client:
|
||||||
|
# No PIN protection is required.
|
||||||
|
self.client.set_expected_responses([proto.Success()])
|
||||||
|
self.client.ping(msg="", pin_protection=True)
|
||||||
|
|
||||||
|
time.sleep(2) # sleep less than the minimal auto-lock delay
|
||||||
|
with self.client:
|
||||||
|
# No PIN protection is required.
|
||||||
|
self.client.set_expected_responses([proto.Success()])
|
||||||
|
self.client.ping(msg="", pin_protection=True)
|
||||||
|
|
||||||
|
time.sleep(10.1) # sleep more than the minimal auto-lock delay
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[proto.PinMatrixRequest(), proto.Success()]
|
||||||
|
)
|
||||||
|
self.client.ping(msg="", pin_protection=True)
|
@ -0,0 +1,50 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib.cardano import get_address
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cardano
|
||||||
|
@pytest.mark.skip_t1 # T1 support is not planned
|
||||||
|
class TestMsgCardanoGetAddress(TrezorTest):
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_address",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"m/44'/1815'/0'/0/0",
|
||||||
|
"Ae2tdPwUPEZLCq3sFv4wVYxwqjMH2nUzBVt1HFr4v87snYrtYq3d3bq2PUQ",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"m/44'/1815'/0'/0/1",
|
||||||
|
"Ae2tdPwUPEZEY6pVJoyuNNdLp7VbMB7U7qfebeJ7XGunk5Z2eHarkcN1bHK",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"m/44'/1815'/0'/0/2",
|
||||||
|
"Ae2tdPwUPEZ3gZD1QeUHvAqadAV59Zid6NP9VCR9BG5LLAja9YtBUgr6ttK",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cardano_get_address(self, path, expected_address):
|
||||||
|
# data from https://iancoleman.io/bip39/#english
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
address = get_address(self.client, parse_path(path))
|
||||||
|
assert address == expected_address
|
@ -0,0 +1,60 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib.cardano import get_public_key
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cardano
|
||||||
|
@pytest.mark.skip_t1 # T1 support is not planned
|
||||||
|
class TestMsgCardanoGetPublicKey(TrezorTest):
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,public_key,chain_code",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"m/44'/1815'/0'",
|
||||||
|
"c0fce1839f1a84c4e770293ac2f5e0875141b29017b7f56ab135352d00ad6966",
|
||||||
|
"07faa161c9f5464315d2855f70fdf1431d5fa39eb838767bf17b69772137452f",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"m/44'/1815'/1'",
|
||||||
|
"ea5dde31b9f551e08a5b6b2f98b8c42c726f726c9ce0a7072102ead53bd8f21e",
|
||||||
|
"70f131bb799fd659c997221ad8cae7dcce4e8da701f8101cf15307fd3a3712a1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"m/44'/1815'/2'",
|
||||||
|
"076338cee5ab3dae19f06ccaa80e3d4428cf0e1bdc04243e41bba7be63a90da7",
|
||||||
|
"5dcdf129f6f2d108292e615c4b67a1fc41a64e6a96130f5c981e5e8e046a6cd7",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"m/44'/1815'/3'",
|
||||||
|
"5f769380dc6fd17a4e0f2d23aa359442a712e5e96d7838ebb91eb020003cccc3",
|
||||||
|
"1197ea234f528987cbac9817ebc31344395b837a3bb7c2332f87e095e70550a5",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cardano_get_public_key(self, path, public_key, chain_code):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
|
||||||
|
key = get_public_key(self.client, parse_path(path))
|
||||||
|
|
||||||
|
assert key.node.public_key.hex() == public_key
|
||||||
|
assert key.node.chain_code.hex() == chain_code
|
||||||
|
assert key.xpub == public_key + chain_code
|
@ -0,0 +1,235 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import cardano, messages
|
||||||
|
from trezorlib.exceptions import TrezorFailure
|
||||||
|
|
||||||
|
from .conftest import setup_client
|
||||||
|
|
||||||
|
PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 1097911063}
|
||||||
|
|
||||||
|
SAMPLE_INPUTS = [
|
||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"path": "m/44'/1815'/0'/0/1",
|
||||||
|
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
|
||||||
|
"prev_index": 0,
|
||||||
|
"type": 0,
|
||||||
|
},
|
||||||
|
"prev_tx": "839f8200d818582482582008abb575fac4c39d5bf80683f7f0c37e48f4e3d96e37d1f6611919a7241b456600ff9f8282d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a00305becffa0",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
VALID_VECTORS = [
|
||||||
|
# Mainnet transaction without change
|
||||||
|
(
|
||||||
|
# protocol magic
|
||||||
|
PROTOCOL_MAGICS["mainnet"],
|
||||||
|
# inputs
|
||||||
|
[SAMPLE_INPUTS[0]["input"]],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
|
||||||
|
"amount": "3003112",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[SAMPLE_INPUTS[0]["prev_tx"]],
|
||||||
|
# tx hash
|
||||||
|
"799c65e8a2c0b1dc4232611728c09d3f3eb0d811c077f8e9798f84605ef1b23d",
|
||||||
|
# tx body
|
||||||
|
"82839f8200d81858248258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00ff9f8282d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e8ffa0818200d818588582584089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea26308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a6355840312c01c27317415b0b8acc86aa789da877fe7e15c65b7ea4c4565d8739117f5f6d9d38bf5d058f7be809b2b9b06c1d79fc6b20f9a4d76d8c89bae333edf5680c",
|
||||||
|
),
|
||||||
|
# Mainnet transaction with change
|
||||||
|
(
|
||||||
|
# protocol magic (mainnet)
|
||||||
|
764824073,
|
||||||
|
# inputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "m/44'/1815'/0'/0/1",
|
||||||
|
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
|
||||||
|
"prev_index": 0,
|
||||||
|
"type": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
|
||||||
|
"amount": "3003112",
|
||||||
|
},
|
||||||
|
{"path": "m/44'/1815'/0'/0/1", "amount": "1000000"},
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[SAMPLE_INPUTS[0]["prev_tx"]],
|
||||||
|
# tx hash
|
||||||
|
"40bf94518f31aba7779dd99aa71fe867887bcb3e0bac2c6dc33d3f20ec74a6b1",
|
||||||
|
# tx body
|
||||||
|
"82839f8200d81858248258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00ff9f8282d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e88282d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a000f4240ffa0818200d818588582584089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea26308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63558400b47193163462023bdb72f03b2f6afc8e3645dbc9252cb70f7516da402ce3b8468e4a60929674de5862d6253315008e07b60aa189f5c455dd272ff1c84c89d0c",
|
||||||
|
),
|
||||||
|
# Testnet transaction
|
||||||
|
(
|
||||||
|
# protocol magic
|
||||||
|
PROTOCOL_MAGICS["testnet"],
|
||||||
|
# inputs
|
||||||
|
[SAMPLE_INPUTS[0]["input"]],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
|
||||||
|
"amount": "3003112",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[SAMPLE_INPUTS[0]["prev_tx"]],
|
||||||
|
# tx hash
|
||||||
|
"799c65e8a2c0b1dc4232611728c09d3f3eb0d811c077f8e9798f84605ef1b23d",
|
||||||
|
# tx body
|
||||||
|
"82839f8200d81858248258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00ff9f8282d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e8ffa0818200d818588582584089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea26308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63558403594ee7e2bfe4c84f886a8336cecb7c42983ce9a057345ebb6294a436087d8db93ca78cf514c7c48edff4c8435f690a5817951e2b55d2db729875ee7cc0f7d08",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
INVALID_VECTORS = [
|
||||||
|
# Output address is a valid CBOR but invalid Cardano address
|
||||||
|
(
|
||||||
|
# protocol magic
|
||||||
|
PROTOCOL_MAGICS["mainnet"],
|
||||||
|
# inputs
|
||||||
|
[SAMPLE_INPUTS[0]["input"]],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "jsK75PTH2esX8k4Wvxenyz83LJJWToBbVmGrWUer2CHFHanLseh7r3sW5X5q",
|
||||||
|
"amount": "3003112",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[SAMPLE_INPUTS[0]["prev_tx"]],
|
||||||
|
"Invalid output address!",
|
||||||
|
),
|
||||||
|
# Output address is an invalid CBOR
|
||||||
|
(
|
||||||
|
# protocol magic
|
||||||
|
PROTOCOL_MAGICS["mainnet"],
|
||||||
|
# inputs
|
||||||
|
[SAMPLE_INPUTS[0]["input"]],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "jsK75PTH2esX8k4Wvxenyz83LJJWToBbVmGrWUer2CHFHanLseh7r3sW5X5q",
|
||||||
|
"amount": "3003112",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[
|
||||||
|
"839f8200d818582482582008abb575fac4c39d5bf80683f7f0c37e48f4e3d96e37d1f6611919a7241b456600ff9f8282d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a00305becffa0"
|
||||||
|
],
|
||||||
|
"Invalid output address!",
|
||||||
|
),
|
||||||
|
# Output address is invalid CBOR
|
||||||
|
(
|
||||||
|
# protocol magic (mainnet)
|
||||||
|
764824073,
|
||||||
|
# inputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "m/44'/1815'/0'/0/1",
|
||||||
|
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
|
||||||
|
"prev_index": 0,
|
||||||
|
"type": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# outputs
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "5dnY6xgRcNUSLGa4gfqef2jGAMHb7koQs9EXErXLNC1LiMPUnhn8joXhvEJpWQtN3F4ysATcBvCn5tABgL3e4hPWapPHmcK5GJMSEaET5JafgAGwSrznzL1Mqa",
|
||||||
|
"amount": "3003112",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
# transactions
|
||||||
|
[SAMPLE_INPUTS[0]["prev_tx"]],
|
||||||
|
"Invalid output address!",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cardano
|
||||||
|
@pytest.mark.skip_t1 # T1 support is not planned
|
||||||
|
@setup_client()
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"protocol_magic,inputs,outputs,transactions,tx_hash,tx_body", VALID_VECTORS
|
||||||
|
)
|
||||||
|
def test_cardano_sign_tx(
|
||||||
|
client, protocol_magic, inputs, outputs, transactions, tx_hash, tx_body
|
||||||
|
):
|
||||||
|
inputs = [cardano.create_input(i) for i in inputs]
|
||||||
|
outputs = [cardano.create_output(o) for o in outputs]
|
||||||
|
|
||||||
|
expected_responses = [
|
||||||
|
messages.CardanoTxRequest(tx_index=i) for i in range(len(transactions))
|
||||||
|
]
|
||||||
|
expected_responses += [
|
||||||
|
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
|
||||||
|
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
|
||||||
|
messages.CardanoSignedTx(),
|
||||||
|
]
|
||||||
|
|
||||||
|
def input_flow():
|
||||||
|
yield
|
||||||
|
client.debug.swipe_down()
|
||||||
|
client.debug.press_yes()
|
||||||
|
yield
|
||||||
|
client.debug.swipe_down()
|
||||||
|
client.debug.press_yes()
|
||||||
|
|
||||||
|
with client:
|
||||||
|
client.set_expected_responses(expected_responses)
|
||||||
|
client.set_input_flow(input_flow)
|
||||||
|
response = cardano.sign_tx(
|
||||||
|
client, inputs, outputs, transactions, protocol_magic
|
||||||
|
)
|
||||||
|
assert response.tx_hash.hex() == tx_hash
|
||||||
|
assert response.tx_body.hex() == tx_body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cardano
|
||||||
|
@pytest.mark.skip_t1 # T1 support is not planned
|
||||||
|
@setup_client()
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"protocol_magic,inputs,outputs,transactions,expected_error_message", INVALID_VECTORS
|
||||||
|
)
|
||||||
|
def test_cardano_sign_tx_validation(
|
||||||
|
client, protocol_magic, inputs, outputs, transactions, expected_error_message
|
||||||
|
):
|
||||||
|
inputs = [cardano.create_input(i) for i in inputs]
|
||||||
|
outputs = [cardano.create_output(o) for o in outputs]
|
||||||
|
|
||||||
|
expected_responses = [
|
||||||
|
messages.CardanoTxRequest(tx_index=i) for i in range(len(transactions))
|
||||||
|
]
|
||||||
|
expected_responses += [messages.Failure()]
|
||||||
|
|
||||||
|
with client:
|
||||||
|
client.set_expected_responses(expected_responses)
|
||||||
|
|
||||||
|
with pytest.raises(TrezorFailure) as exc:
|
||||||
|
cardano.sign_tx(client, inputs, outputs, transactions, protocol_magic)
|
||||||
|
|
||||||
|
assert exc.value.args[1] == expected_error_message
|
219
python/trezorlib/tests/device_tests/test_msg_changepin.py
Normal file
219
python/trezorlib/tests/device_tests/test_msg_changepin.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import messages as proto
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
class TestMsgChangepin(TrezorTest):
|
||||||
|
def test_set_pin(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is False
|
||||||
|
|
||||||
|
# Check that there's no PIN protection
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
# Let's set new PIN
|
||||||
|
ret = self.client.call_raw(proto.ChangePin())
|
||||||
|
assert isinstance(ret, proto.ButtonRequest)
|
||||||
|
|
||||||
|
# Press button
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
ret = self.client.call_raw(proto.ButtonAck())
|
||||||
|
|
||||||
|
# Send the PIN for first time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send the PIN for second time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Now we're done
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
# Check that there's PIN protection now
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
|
||||||
|
# Check that the PIN is correct
|
||||||
|
self.check_pin(self.pin6)
|
||||||
|
|
||||||
|
def test_change_pin(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
|
||||||
|
# Check that there's PIN protection
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
self.client.call_raw(proto.Cancel())
|
||||||
|
|
||||||
|
# Check current PIN value
|
||||||
|
self.check_pin(self.pin4)
|
||||||
|
|
||||||
|
# Let's change PIN
|
||||||
|
ret = self.client.call_raw(proto.ChangePin())
|
||||||
|
assert isinstance(ret, proto.ButtonRequest)
|
||||||
|
|
||||||
|
# Press button
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
ret = self.client.call_raw(proto.ButtonAck())
|
||||||
|
|
||||||
|
# Send current PIN
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.read_pin_encoded()
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send new PIN for first time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send the PIN for second time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Now we're done
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
# Check that there's still PIN protection now
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
|
||||||
|
# Check that the PIN is correct
|
||||||
|
self.check_pin(self.pin6)
|
||||||
|
|
||||||
|
def test_remove_pin(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
|
||||||
|
# Check that there's PIN protection
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
self.client.call_raw(proto.Cancel())
|
||||||
|
|
||||||
|
# Let's remove PIN
|
||||||
|
ret = self.client.call_raw(proto.ChangePin(remove=True))
|
||||||
|
assert isinstance(ret, proto.ButtonRequest)
|
||||||
|
|
||||||
|
# Press button
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
ret = self.client.call_raw(proto.ButtonAck())
|
||||||
|
|
||||||
|
# Send current PIN
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.read_pin_encoded()
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Now we're done
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
# Check that there's no PIN protection now
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is False
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
def test_set_failed(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is False
|
||||||
|
|
||||||
|
# Check that there's no PIN protection
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
# Let's set new PIN
|
||||||
|
ret = self.client.call_raw(proto.ChangePin())
|
||||||
|
assert isinstance(ret, proto.ButtonRequest)
|
||||||
|
|
||||||
|
# Press button
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
ret = self.client.call_raw(proto.ButtonAck())
|
||||||
|
|
||||||
|
# Send the PIN for first time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send the PIN for second time, but with typo
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin4)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Now it should fail, because pins are different
|
||||||
|
assert isinstance(ret, proto.Failure)
|
||||||
|
|
||||||
|
# Check that there's still no PIN protection now
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is False
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.Success)
|
||||||
|
|
||||||
|
def test_set_failed_2(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
|
||||||
|
# Let's set new PIN
|
||||||
|
ret = self.client.call_raw(proto.ChangePin())
|
||||||
|
assert isinstance(ret, proto.ButtonRequest)
|
||||||
|
|
||||||
|
# Press button
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
ret = self.client.call_raw(proto.ButtonAck())
|
||||||
|
|
||||||
|
# Send current PIN
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.read_pin_encoded()
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send the PIN for first time
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Send the PIN for second time, but with typo
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(self.pin6 + "3")
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
|
||||||
|
# Now it should fail, because pins are different
|
||||||
|
assert isinstance(ret, proto.Failure)
|
||||||
|
|
||||||
|
# Check that there's still old PIN protection
|
||||||
|
features = self.client.call_raw(proto.Initialize())
|
||||||
|
assert features.pin_protection is True
|
||||||
|
self.check_pin(self.pin4)
|
||||||
|
|
||||||
|
def check_pin(self, pin):
|
||||||
|
self.client.clear_session()
|
||||||
|
ret = self.client.call_raw(proto.Ping(pin_protection=True))
|
||||||
|
assert isinstance(ret, proto.PinMatrixRequest)
|
||||||
|
pin_encoded = self.client.debug.encode_pin(pin)
|
||||||
|
ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||||
|
assert isinstance(ret, proto.Success)
|
192
python/trezorlib/tests/device_tests/test_msg_cipherkeyvalue.py
Normal file
192
python/trezorlib/tests/device_tests/test_msg_cipherkeyvalue.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import misc
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgCipherkeyvalue(TrezorTest):
|
||||||
|
def test_encrypt(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
# different ask values
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res.hex() == "676faf8f13272af601776bc31bc14e8f"
|
||||||
|
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=False,
|
||||||
|
)
|
||||||
|
assert res.hex() == "5aa0fbcb9d7fa669880745479d80c622"
|
||||||
|
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=False,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res.hex() == "958d4f63269b61044aaedc900c8d6208"
|
||||||
|
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=False,
|
||||||
|
ask_on_decrypt=False,
|
||||||
|
)
|
||||||
|
assert res.hex() == "e0cf0eb0425947000eb546cc3994bc6c"
|
||||||
|
|
||||||
|
# different key
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test2",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res.hex() == "de247a6aa6be77a134bb3f3f925f13af"
|
||||||
|
|
||||||
|
# different message
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
b"testing message! it is different",
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
res.hex()
|
||||||
|
== "676faf8f13272af601776bc31bc14e8f3ae1c88536bf18f1b44f1e4c2c4a613d"
|
||||||
|
)
|
||||||
|
|
||||||
|
# different path
|
||||||
|
res = misc.encrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 3],
|
||||||
|
b"test",
|
||||||
|
b"testing message!",
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res.hex() == "b4811a9d492f5355a5186ddbfccaae7b"
|
||||||
|
|
||||||
|
def test_decrypt(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
# different ask values
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex("676faf8f13272af601776bc31bc14e8f"),
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex("5aa0fbcb9d7fa669880745479d80c622"),
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=False,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex("958d4f63269b61044aaedc900c8d6208"),
|
||||||
|
ask_on_encrypt=False,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex("e0cf0eb0425947000eb546cc3994bc6c"),
|
||||||
|
ask_on_encrypt=False,
|
||||||
|
ask_on_decrypt=False,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
# different key
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test2",
|
||||||
|
bytes.fromhex("de247a6aa6be77a134bb3f3f925f13af"),
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
# different message
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 2],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex(
|
||||||
|
"676faf8f13272af601776bc31bc14e8f3ae1c88536bf18f1b44f1e4c2c4a613d"
|
||||||
|
),
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res == b"testing message! it is different"
|
||||||
|
|
||||||
|
# different path
|
||||||
|
res = misc.decrypt_keyvalue(
|
||||||
|
self.client,
|
||||||
|
[0, 1, 3],
|
||||||
|
b"test",
|
||||||
|
bytes.fromhex("b4811a9d492f5355a5186ddbfccaae7b"),
|
||||||
|
ask_on_encrypt=True,
|
||||||
|
ask_on_decrypt=True,
|
||||||
|
)
|
||||||
|
assert res == b"testing message!"
|
||||||
|
|
||||||
|
def test_encrypt_badlen(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
misc.encrypt_keyvalue(self.client, [0, 1, 2], b"test", b"testing")
|
||||||
|
|
||||||
|
def test_decrypt_badlen(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
misc.decrypt_keyvalue(self.client, [0, 1, 2], b"test", b"testing")
|
96
python/trezorlib/tests/device_tests/test_msg_clearsession.py
Normal file
96
python/trezorlib/tests/device_tests/test_msg_clearsession.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import messages as proto
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t2
|
||||||
|
class TestMsgClearsession(TrezorTest):
|
||||||
|
def test_clearsession(self):
|
||||||
|
self.setup_mnemonic_pin_passphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||||
|
proto.PinMatrixRequest(),
|
||||||
|
proto.PassphraseRequest(),
|
||||||
|
proto.Success(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res = self.client.ping(
|
||||||
|
"random data",
|
||||||
|
button_protection=True,
|
||||||
|
pin_protection=True,
|
||||||
|
passphrase_protection=True,
|
||||||
|
)
|
||||||
|
assert res == "random data"
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
# pin and passphrase are cached
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||||
|
proto.Success(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res = self.client.ping(
|
||||||
|
"random data",
|
||||||
|
button_protection=True,
|
||||||
|
pin_protection=True,
|
||||||
|
passphrase_protection=True,
|
||||||
|
)
|
||||||
|
assert res == "random data"
|
||||||
|
|
||||||
|
self.client.clear_session()
|
||||||
|
|
||||||
|
# session cache is cleared
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||||
|
proto.PinMatrixRequest(),
|
||||||
|
proto.PassphraseRequest(),
|
||||||
|
proto.Success(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res = self.client.ping(
|
||||||
|
"random data",
|
||||||
|
button_protection=True,
|
||||||
|
pin_protection=True,
|
||||||
|
passphrase_protection=True,
|
||||||
|
)
|
||||||
|
assert res == "random data"
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
# pin and passphrase are cached
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||||
|
proto.Success(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res = self.client.ping(
|
||||||
|
"random data",
|
||||||
|
button_protection=True,
|
||||||
|
pin_protection=True,
|
||||||
|
passphrase_protection=True,
|
||||||
|
)
|
||||||
|
assert res == "random data"
|
@ -0,0 +1,52 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumGetaddress(TrezorTest):
|
||||||
|
def test_ethereum_getaddress(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(60)])
|
||||||
|
== "0xE025dfbE2C53638E547C6487DED34Add7b8Aafc1"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(60), 1])
|
||||||
|
== "0xeD46C856D0c79661cF7d40FFE0C0C5077c00E898"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(60), 0, H_(1)])
|
||||||
|
== "0x6682Fa7F3eC58581b1e576268b5463B4b5c93839"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(60), H_(9), 0])
|
||||||
|
== "0xFb3BE0F9717fF5fCF3C58EB49a9Ed67F1BD89D4E"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(60), 0, 9999999])
|
||||||
|
== "0x6b909b50d88c9A8E02453A87b3662E3e7a5E0CF1"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ethereum.get_address(self.client, [H_(44), H_(6060), 0, 9999999])
|
||||||
|
== "0x98b8e926bd224764De2A0E4f4CBe1521474050AF"
|
||||||
|
)
|
@ -0,0 +1,44 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumGetPublicKey(TrezorTest):
|
||||||
|
def test_ethereum_getpublickey(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
res = ethereum.get_public_node(self.client, [H_(44), H_(60), H_(0)])
|
||||||
|
assert res.node.depth == 3
|
||||||
|
assert res.node.fingerprint == 0xC10CFFDA
|
||||||
|
assert res.node.child_num == 0x80000000
|
||||||
|
assert (
|
||||||
|
res.node.chain_code.hex()
|
||||||
|
== "813d9feda6421f97a6472ff36679aa9e211ff88f6bdee51093af313ce628087e"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
res.node.public_key.hex()
|
||||||
|
== "0318c22dedce01caca32354f98428e3af06a452f3fa84e6af8f1b6aa362affa641"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
res.xpub
|
||||||
|
== "xpub6D54vV8eUYHMVBZCnz4SLjuiQngXURVCGKKGoJrWUDRegdMByLTJKfRs64q3UKiQCsSHJPtCQehTvERczdghS7gb8oedWSyNDtBU1zYDJtb"
|
||||||
|
)
|
@ -0,0 +1,46 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumSignmessage(TrezorTest):
|
||||||
|
|
||||||
|
PATH = [H_(44), H_(60), H_(0), 0]
|
||||||
|
ADDRESS = "0xEa53AF85525B1779eE99ece1a5560C0b78537C3b"
|
||||||
|
VECTORS = [
|
||||||
|
(
|
||||||
|
"This is an example of a signed message.",
|
||||||
|
"9bacd833b51fde010bab53bafd9d832eadd3b175d2af2e629bb2944fcc987dce7ff68bb3571ed25a720c220f2f9538bc8d04f582bee002c9af086590a49805901c",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"VeryLongMessage!" * 64,
|
||||||
|
"752d283b3aea1eb44fd09203f4d5c430a6544e399b8500b02722b54325f6d8d457fd83460a31045cb0d6e8356240954ba072fdfe5cdb3f16d416e2acf1a180a51c",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_sign(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
for msg, sig in self.VECTORS:
|
||||||
|
res = ethereum.sign_message(self.client, self.PATH, msg)
|
||||||
|
assert res.address == self.ADDRESS
|
||||||
|
assert res.signature.hex() == sig
|
407
python/trezorlib/tests/device_tests/test_msg_ethereum_signtx.py
Normal file
407
python/trezorlib/tests/device_tests/test_msg_ethereum_signtx.py
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum, messages as proto
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
TO_ADDR = "0x1d1c328764a41bda0492b66baa30c4a339ff85ef"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumSigntx(TrezorTest):
|
||||||
|
def test_ethereum_signtx_known_erc20_token(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(data_length=None),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
# method id signalizing `transfer(address _to, uint256 _value)` function
|
||||||
|
data.extend(bytes.fromhex("a9059cbb"))
|
||||||
|
# 1st function argument (to - the receiver)
|
||||||
|
data.extend(
|
||||||
|
bytes.fromhex(
|
||||||
|
"000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 2nd function argument (value - amount to be transferred)
|
||||||
|
data.extend(
|
||||||
|
bytes.fromhex(
|
||||||
|
"000000000000000000000000000000000000000000000000000000000bebc200"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 200 000 000 in dec, divisibility of ADT = 9, trezor1 displays 0.2 ADT, Trezor T 200 000 000 Wei ADT
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20,
|
||||||
|
gas_limit=20,
|
||||||
|
# ADT token address
|
||||||
|
to="0xd0d6d6c5fe4a677d343cc433536bb717bae167dd",
|
||||||
|
chain_id=1,
|
||||||
|
# value needs to be 0, token value is set in the contract (data)
|
||||||
|
value=0,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# taken from T1 might not be 100% correct but still better than nothing
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "ec1df922115d256745410fbc2070296756583c8786e4d402a88d4e29ec513fa9"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "7001bfe3ba357e4a9f9e0d3a3f8a8962257615a4cf215db93e48b98999fc51b7"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_signtx_unknown_erc20_token(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(data_length=None),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
# method id signalizing `transfer(address _to, uint256 _value)` function
|
||||||
|
data.extend(bytes.fromhex("a9059cbb"))
|
||||||
|
# 1st function argument (to - the receiver)
|
||||||
|
data.extend(
|
||||||
|
bytes.fromhex(
|
||||||
|
"000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 2nd function argument (value - amount to be transferred)
|
||||||
|
data.extend(
|
||||||
|
bytes.fromhex(
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000123"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# since this token is unknown trezor should display "unknown token value"
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/1"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20,
|
||||||
|
gas_limit=20,
|
||||||
|
# unknown token address (Grzegorz Brzęczyszczykiewicz Token)
|
||||||
|
to="0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
|
||||||
|
chain_id=1,
|
||||||
|
# value needs to be 0, token value is set in the contract (data)
|
||||||
|
value=0,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# taken from T1 might not be 100% correct but still better than nothing
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "2559bbf1bcb80992b6eaa96f0074b19606d8ea7bf4219e1c9ac64a12855c0cce"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "633a74429eb6d3aeec4ed797542236a85daab3cab15e37736b87a45697541d7a"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_signtx_nodata(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(data_length=None), # v,r,s checked later
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/100"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20,
|
||||||
|
gas_limit=20,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sig_v == 27
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "2f548f63ddb4cf19b6b9f922da58ff71833b967d590f3b4dcc2a70810338a982"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "428d35f0dca963b5196b63e7aa5e0405d8bff77d6aee1202183f1f68dacb4483"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(data_length=None),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/100"),
|
||||||
|
nonce=123456,
|
||||||
|
gas_price=20000,
|
||||||
|
gas_limit=20000,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
||||||
|
assert sig_v == 27
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "3bf0470cd7f5ad8d82613199f73deadc55c3c9f32f91b1a21b5ef644144ebd58"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "48b3ef1b2502febdf35e9ff4df0ba1fda62f042fad639eb4852a297fc9872ebd"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_signtx_data(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(data_length=None),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20,
|
||||||
|
gas_limit=20,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=10,
|
||||||
|
data=b"abcdefghijklmnop" * 16,
|
||||||
|
)
|
||||||
|
assert sig_v == 27
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "e90f9e3dbfb34861d40d67570cb369049e675c6eebfdda6b08413a2283421b85"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "763912b8801f76cbea7792d98123a245514beeab2f3afebb4bab637888e8393a"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(
|
||||||
|
data_length=1024,
|
||||||
|
signature_r=None,
|
||||||
|
signature_s=None,
|
||||||
|
signature_v=None,
|
||||||
|
),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=3),
|
||||||
|
proto.EthereumTxRequest(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=123456,
|
||||||
|
gas_price=20000,
|
||||||
|
gas_limit=20000,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
data=b"ABCDEFGHIJKLMNOP" * 256 + b"!!!",
|
||||||
|
)
|
||||||
|
assert sig_v == 27
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "dd96d82d791118a55601dfcede237760d2e9734b76c373ede5362a447c42ac48"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "60a77558f28d483d476f9507cd8a6a4bb47b86611aaff95fd5499b9ee9ebe7ee"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_signtx_message(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(
|
||||||
|
data_length=1024,
|
||||||
|
signature_r=None,
|
||||||
|
signature_s=None,
|
||||||
|
signature_v=None,
|
||||||
|
),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=3),
|
||||||
|
proto.EthereumTxRequest(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20000,
|
||||||
|
gas_limit=20000,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=0,
|
||||||
|
data=b"ABCDEFGHIJKLMNOP" * 256 + b"!!!",
|
||||||
|
)
|
||||||
|
assert sig_v == 27
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "81af16020d3c6ad820cab2e2b0834fa37f4a9b0c2443f151a4e2f12fe1081b09"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "7b34b5d8a43771d493cd9fa0c7b27a9563e2a31799fb9f0c2809539a848b9f47"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_signtx_newcontract(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
|
||||||
|
# contract creation without data should fail.
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=123456,
|
||||||
|
gas_price=20000,
|
||||||
|
gas_limit=20000,
|
||||||
|
to="",
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.client:
|
||||||
|
self.client.set_expected_responses(
|
||||||
|
[
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
|
||||||
|
proto.EthereumTxRequest(
|
||||||
|
data_length=1024,
|
||||||
|
signature_r=None,
|
||||||
|
signature_s=None,
|
||||||
|
signature_v=None,
|
||||||
|
),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=1024),
|
||||||
|
proto.EthereumTxRequest(data_length=3),
|
||||||
|
proto.EthereumTxRequest(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=0,
|
||||||
|
gas_price=20000,
|
||||||
|
gas_limit=20000,
|
||||||
|
to="",
|
||||||
|
value=12345678901234567890,
|
||||||
|
data=b"ABCDEFGHIJKLMNOP" * 256 + b"!!!",
|
||||||
|
)
|
||||||
|
assert sig_v == 28
|
||||||
|
assert (
|
||||||
|
sig_r.hex()
|
||||||
|
== "c86bda9de238b1c602648996561e7270a3be208da96bbf23474cb8e4014b9f93"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sig_s.hex()
|
||||||
|
== "18742403f75a05e7fa9868c30b36f1e55628de02d01c03084c1ff6775a13137c"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ethereum_sanity_checks(self):
|
||||||
|
# gas overflow
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=parse_path("44'/60'/0'/0/0"),
|
||||||
|
nonce=123456,
|
||||||
|
gas_price=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
|
||||||
|
gas_limit=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
||||||
|
|
||||||
|
# no gas price
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=[0, 0],
|
||||||
|
nonce=123456,
|
||||||
|
gas_limit=10000,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
||||||
|
|
||||||
|
# no gas limit
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=[0, 0],
|
||||||
|
nonce=123456,
|
||||||
|
gas_price=10000,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
||||||
|
|
||||||
|
# no nonce
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=[0, 0],
|
||||||
|
gas_price=10000,
|
||||||
|
gas_limit=123456,
|
||||||
|
to=TO_ADDR,
|
||||||
|
value=12345678901234567890,
|
||||||
|
)
|
@ -0,0 +1,219 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumSigntxChainId(TrezorTest):
|
||||||
|
def test_ethereum_signtx_eip155(self):
|
||||||
|
|
||||||
|
# chain_id, nonce, sig_v, sig_r, sig_s, value, gas_limit, data
|
||||||
|
VECTORS = [
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
"cde31d8ab07d423d5e52aeb148180528ea54974cdb4c5578499c0137ec24d892",
|
||||||
|
"41fc58955b3b3e3f3b2aced65e11e8a3cb6339027f943bec3d504d6398b69dd2",
|
||||||
|
100000000000000000,
|
||||||
|
21000,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
41,
|
||||||
|
"57951fed170f3765dea164d65acd31373799db32ec572e213b1d9a1209956b98",
|
||||||
|
"0971f8830c0e2e89919309f217ed2eadb0c63d647e016d220729ce79d27c24a0",
|
||||||
|
100000000000000000,
|
||||||
|
21000,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
42,
|
||||||
|
"73744f66231690edd9eed2ab3c2b56ec4f6c4b9aabc633ae7f3f4ea94223d52c",
|
||||||
|
"7f500afbe2b2b4e4e57f22511e3a42b3596b85cad7fe1eca700cdae1905d3555",
|
||||||
|
100000000000000000,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
42,
|
||||||
|
"1a4fc1ec5f98bf874d5336aaf1fa9069ce68dc36c3f77e93465c9ac2c8b4b741",
|
||||||
|
"13007c9b1df6a0d2f2ffa9d0ebcdec189122a5e781eb64967eb0d6a6def95b7a",
|
||||||
|
100000000000000000,
|
||||||
|
299732,
|
||||||
|
b"ABCDEFGHIJKLMNOP" * 256 + b"!!!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
42,
|
||||||
|
"8da0358d780df542f767d977f99ad034b6d9fa808fe50997141be2a1b93542c0",
|
||||||
|
"2dafe1ead8aae1051e6662c5d553b34067bda9c8fa7314ae8693ec61ddfc96d4",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
38,
|
||||||
|
"b72707f0f5a38339c9dd0359720312c739a8ac6554659c7af48456f06ba33374",
|
||||||
|
"75a431c046046942f9c1f3305cd08f34302164811c675ac0a0ac0b73cb30a90e",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
255,
|
||||||
|
1,
|
||||||
|
545,
|
||||||
|
"529172fb644a6d29b7218fb783f3d666021fc29cc4bf9bffbcfb3b84ab8d6181",
|
||||||
|
"30980c6102a12872ef9cd888f2bf90c81bbbdc8878ff7d1d1382f8983b0d0c49",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
256,
|
||||||
|
1,
|
||||||
|
548,
|
||||||
|
"db53c05c679bdfdf3ded787ce9607d3f109ae46c87b1dcc9ab34053e5ed0eace",
|
||||||
|
"39645dd48118d369b588dbf279f1a8c01051fabf65bf8eaa633c6433ff120cce",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
65535,
|
||||||
|
1,
|
||||||
|
131105,
|
||||||
|
"b520fa77767cdf07b6014d4a8fb35eebe5ed7c0edab97132b0dc74e3e1f13ed9",
|
||||||
|
"78735b2db4cf95fb651c5c1f5529e60542019e456c6cb7a9f4bd9bbb83418d99",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
65536,
|
||||||
|
1,
|
||||||
|
131107,
|
||||||
|
"4b6122ba875b57ce084bd5f08e9ae1944e998726a4056c9b7746432d8f46ba99",
|
||||||
|
"6812c2668ac9c9927b69ef7cf9baec54436f7319ccc14f0f664e1e94e6109e06",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
16777215,
|
||||||
|
1,
|
||||||
|
33554465,
|
||||||
|
"68a8c6f2336a8e3296f17a307d84a1e6d3ab1383fdcc62611c2e8426f2e2777e",
|
||||||
|
"2d4ce900077ab40aac26064945998dbac5a014baadae2d3cb629cdeb9452db61",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
16777216,
|
||||||
|
1,
|
||||||
|
33554468,
|
||||||
|
"b6c42c584ef69621a2e5f3e1ab9dad890dbff3c92a599230dd0e394cd29d1c68",
|
||||||
|
"497eec05ea52773d0f05e7fdf4f7993b3a06ef958804b39af699ef09ed0f5d7e",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2147483629,
|
||||||
|
1,
|
||||||
|
4294967294,
|
||||||
|
"1a31f886c0bba527e622a731270dc29e62a607ff63558fca38745e5b9a672686",
|
||||||
|
"0f3fce8a70598bbb54387cde7e8f957a27e4a816cbc9408717b27d8666222bd9",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
b"\0",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2147483630,
|
||||||
|
1,
|
||||||
|
4294967296,
|
||||||
|
"ba6cb6e2ebbac3726db9a3e4a939454009108f6515330e567aeada14ecebe074",
|
||||||
|
"2bbfba1154cae32e3e6c6bbf3ce41cba6cc8c6b764245ba6026605506838e690",
|
||||||
|
0,
|
||||||
|
21004,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2147483631,
|
||||||
|
1,
|
||||||
|
4294967298,
|
||||||
|
"3c743528e9ce315db02e487de93f2b2cfc93421e43f1d519f77a2f05bd2ce190",
|
||||||
|
"16c1fec1495fe5da89d1a026f1a575ff354e18ff0fb9d04a6cfb0413267ab2bc",
|
||||||
|
100000000000000000,
|
||||||
|
21000,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3125659152,
|
||||||
|
1,
|
||||||
|
6251318340,
|
||||||
|
"82cde0c9e1a94c1305791b09e1bcd021a49b036a16d9733acbc1a08bb30f3410",
|
||||||
|
"472c8897519ba410b86f80993236d992e18e94d1f59c3d8760d2d7c90914dfc6",
|
||||||
|
1,
|
||||||
|
21005,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
4294967295,
|
||||||
|
1,
|
||||||
|
8589934625,
|
||||||
|
"67788e892fb372bba16823e16d3186f67494d7b1128555248f3661ad87e9d7ef",
|
||||||
|
"2faf9f06dfdf23ceca2796cf0d55c88187f199e98a94dfb15722824b244d81a1",
|
||||||
|
100000000000000000,
|
||||||
|
21000,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
|
||||||
|
for ci, n, sv, sr, ss, v, gl, d in VECTORS:
|
||||||
|
sig_v, sig_r, sig_s = ethereum.sign_tx(
|
||||||
|
self.client,
|
||||||
|
n=[H_(44), H_(60), H_(0), 0, 0],
|
||||||
|
nonce=n,
|
||||||
|
gas_price=20000000000,
|
||||||
|
gas_limit=gl,
|
||||||
|
to="0x8eA7a3fccC211ED48b763b4164884DDbcF3b0A98",
|
||||||
|
value=v,
|
||||||
|
chain_id=ci,
|
||||||
|
data=d,
|
||||||
|
)
|
||||||
|
assert sig_v == sv
|
||||||
|
assert sig_r.hex() == sr
|
||||||
|
assert sig_s.hex() == ss
|
@ -0,0 +1,53 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import ethereum
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.ethereum
|
||||||
|
class TestMsgEthereumVerifymessage(TrezorTest):
|
||||||
|
|
||||||
|
ADDRESS = "0xCb3864960e8DB1A751212c580AF27Ee8867d688F"
|
||||||
|
VECTORS = [
|
||||||
|
(
|
||||||
|
"This is an example of a signed message.",
|
||||||
|
"b7837058907192dbc9427bf57d93a0acca3816c92927a08be573b785f2d72dab65dad9c92fbe03a358acdb455eab2107b869945d11f4e353d9cc6ea957d08a871b",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"VeryLongMessage!" * 64,
|
||||||
|
"da2b73b0170479c2bfba3dd4839bf0d67732a44df8c873f3f3a2aca8a57d7bdc0b5d534f54c649e2d44135717001998b176d3cd1212366464db51f5838430fb31c",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_verify(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
for msg, sig in self.VECTORS:
|
||||||
|
res = ethereum.verify_message(
|
||||||
|
self.client, self.ADDRESS, bytes.fromhex(sig), msg
|
||||||
|
)
|
||||||
|
assert res is True
|
||||||
|
|
||||||
|
def test_verify_invalid(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
signature = bytes.fromhex(self.VECTORS[0][1])
|
||||||
|
res = ethereum.verify_message(
|
||||||
|
self.client, self.ADDRESS, signature, "another message"
|
||||||
|
)
|
||||||
|
assert res is False
|
218
python/trezorlib/tests/device_tests/test_msg_getaddress.py
Normal file
218
python/trezorlib/tests/device_tests/test_msg_getaddress.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import btc, messages as proto
|
||||||
|
from trezorlib.tools import H_, CallException, parse_path
|
||||||
|
|
||||||
|
from ..support import ckd_public as bip32
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
def getmultisig(chain, nr, xpubs, signatures=[b"", b"", b""]):
|
||||||
|
return proto.MultisigRedeemScriptType(
|
||||||
|
nodes=[bip32.deserialize(xpub) for xpub in xpubs],
|
||||||
|
address_n=[chain, nr],
|
||||||
|
signatures=signatures,
|
||||||
|
m=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetaddress(TrezorTest):
|
||||||
|
def test_btc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [])
|
||||||
|
== "1EfKbQupktEMXf4gujJ9kCFo83k1iMqwqK"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [1])
|
||||||
|
== "1CK7SJdcb8z9HuvVft3D91HLpLC6KSsGb"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [0, H_(1)])
|
||||||
|
== "1JVq66pzRBvqaBRFeU9SPVvg3er4ZDgoMs"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [H_(9), 0])
|
||||||
|
== "1F4YdQdL9ZQwvcNTuy5mjyQxXkyCfMcP2P"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [0, 9999999])
|
||||||
|
== "1GS8X3yc7ntzwGw9vXwj9wqmBWZkTFewBV"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ltc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Litecoin", [])
|
||||||
|
== "LYtGrdDeqYUQnTkr5sHT2DKZLG7Hqg7HTK"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Litecoin", [1])
|
||||||
|
== "LKRGNecThFP3Q6c5fosLVA53Z2hUDb1qnE"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Litecoin", [0, H_(1)])
|
||||||
|
== "LcinMK8pVrAtpz7Qpc8jfWzSFsDLgLYfG6"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Litecoin", [H_(9), 0])
|
||||||
|
== "LZHVtcwAEDf1BR4d67551zUijyLUpDF9EX"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Litecoin", [0, 9999999])
|
||||||
|
== "Laf5nGHSCT94C5dK6fw2RxuXPiw2ZuRR9S"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tbtc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Testnet", [111, 42])
|
||||||
|
== "moN6aN6NP1KWgnPSqzrrRPvx2x1UtZJssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bch(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bcash", parse_path("44'/145'/0'/0/0"))
|
||||||
|
== "bitcoincash:qr08q88p9etk89wgv05nwlrkm4l0urz4cyl36hh9sv"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bcash", parse_path("44'/145'/0'/0/1"))
|
||||||
|
== "bitcoincash:qr23ajjfd9wd73l87j642puf8cad20lfmqdgwvpat4"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bcash", parse_path("44'/145'/0'/1/0"))
|
||||||
|
== "bitcoincash:qzc5q87w069lzg7g3gzx0c8dz83mn7l02scej5aluw"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_grs(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Groestlcoin", parse_path("44'/17'/0'/0/0"))
|
||||||
|
== "Fj62rBJi8LvbmWu2jzkaUX1NFXLEqDLoZM"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Groestlcoin", parse_path("44'/17'/0'/1/0"))
|
||||||
|
== "FmRaqvVBRrAp2Umfqx9V1ectZy8gw54QDN"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Groestlcoin", parse_path("44'/17'/0'/1/1"))
|
||||||
|
== "Fmhtxeh7YdCBkyQF7AQG4QnY8y3rJg89di"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_multisig(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
xpubs = []
|
||||||
|
for n in range(1, 4):
|
||||||
|
node = btc.get_public_node(self.client, parse_path("44'/0'/%d'" % n))
|
||||||
|
xpubs.append(node.xpub)
|
||||||
|
|
||||||
|
for nr in range(1, 4):
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bitcoin",
|
||||||
|
parse_path("44'/0'/%d'/0/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(0, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
== "3Pdz86KtfJBuHLcSv4DysJo4aQfanTqCzG"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bitcoin",
|
||||||
|
parse_path("44'/0'/%d'/1/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(1, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
== "36gP3KVx1ooStZ9quZDXbAF3GCr42b2zzd"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_multisig_missing(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
xpubs = []
|
||||||
|
for n in range(1, 4):
|
||||||
|
# shift account numbers by 10 to create valid multisig,
|
||||||
|
# but not containing the keys used below
|
||||||
|
n = n + 10
|
||||||
|
node = btc.get_public_node(self.client, parse_path("44'/0'/%d'" % n))
|
||||||
|
xpubs.append(node.xpub)
|
||||||
|
for nr in range(1, 4):
|
||||||
|
with pytest.raises(CallException):
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bitcoin",
|
||||||
|
parse_path("44'/0'/%d'/0/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(0, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
with pytest.raises(CallException):
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bitcoin",
|
||||||
|
parse_path("44'/0'/%d'/1/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(1, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bch_multisig(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
xpubs = []
|
||||||
|
for n in range(1, 4):
|
||||||
|
node = btc.get_public_node(self.client, parse_path("44'/145'/%d'" % n))
|
||||||
|
xpubs.append(node.xpub)
|
||||||
|
|
||||||
|
for nr in range(1, 4):
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bcash",
|
||||||
|
parse_path("44'/145'/%d'/0/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(0, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
== "bitcoincash:pqguz4nqq64jhr5v3kvpq4dsjrkda75hwy86gq0qzw"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Bcash",
|
||||||
|
parse_path("44'/145'/%d'/1/0" % nr),
|
||||||
|
show_display=(nr == 1),
|
||||||
|
multisig=getmultisig(1, 0, xpubs=xpubs),
|
||||||
|
)
|
||||||
|
== "bitcoincash:pp6kcpkhua7789g2vyj0qfkcux3yvje7euhyhltn0a"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_public_ckd(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
node = btc.get_public_node(self.client, []).node
|
||||||
|
node_sub1 = btc.get_public_node(self.client, [1]).node
|
||||||
|
node_sub2 = bip32.public_ckd(node, [1])
|
||||||
|
|
||||||
|
assert node_sub1.chain_code == node_sub2.chain_code
|
||||||
|
assert node_sub1.public_key == node_sub2.public_key
|
||||||
|
|
||||||
|
address1 = btc.get_address(self.client, "Bitcoin", [1])
|
||||||
|
address2 = bip32.get_address(node_sub2, 0)
|
||||||
|
|
||||||
|
assert address2 == "1CK7SJdcb8z9HuvVft3D91HLpLC6KSsGb"
|
||||||
|
assert address1 == address2
|
@ -0,0 +1,113 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import btc, messages as proto
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from ..support import ckd_public as bip32
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetaddressSegwit(TrezorTest):
|
||||||
|
def test_show_segwit(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("49'/1'/0'/1/0"),
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
)
|
||||||
|
== "2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("49'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
)
|
||||||
|
== "2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("44'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
)
|
||||||
|
== "2N6UeBoqYEEnybg4cReFYDammpsyDw8R2Mc"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("44'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDADDRESS,
|
||||||
|
)
|
||||||
|
== "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Groestlcoin Testnet",
|
||||||
|
parse_path("49'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
)
|
||||||
|
== "2N4Q5FhU2497BryFfUgbqkAJE87aKDv3V3e"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_show_multisig_3(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
nodes = map(
|
||||||
|
lambda index: btc.get_public_node(
|
||||||
|
self.client, parse_path("999'/1'/%d'" % index)
|
||||||
|
),
|
||||||
|
range(1, 4),
|
||||||
|
)
|
||||||
|
multisig1 = proto.MultisigRedeemScriptType(
|
||||||
|
nodes=[bip32.deserialize(n.xpub) for n in nodes],
|
||||||
|
address_n=[2, 0],
|
||||||
|
signatures=[b"", b"", b""],
|
||||||
|
m=2,
|
||||||
|
)
|
||||||
|
# multisig2 = proto.MultisigRedeemScriptType(
|
||||||
|
# pubkeys=map(lambda n: proto.HDNodePathType(node=bip32.deserialize(n.xpub), address_n=[2, 1]), nodes),
|
||||||
|
# signatures=[b'', b'', b''],
|
||||||
|
# m=2,
|
||||||
|
# )
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("999'/1'/%d'/2/0" % i),
|
||||||
|
False,
|
||||||
|
multisig1,
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
)
|
||||||
|
== "2N2MxyAfifVhb3AMagisxaj3uij8bfXqf4Y"
|
||||||
|
)
|
@ -0,0 +1,123 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import btc, messages as proto
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from ..support import ckd_public as bip32
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetaddressSegwitNative(TrezorTest):
|
||||||
|
def test_show_segwit(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("49'/1'/0'/0/0"),
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "tb1qqzv60m9ajw8drqulta4ld4gfx0rdh82un5s65s"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("49'/1'/0'/1/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("44'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "tb1q54un3q39sf7e7tlfq99d6ezys7qgc62a6rxllc"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("44'/1'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDADDRESS,
|
||||||
|
)
|
||||||
|
== "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Groestlcoin",
|
||||||
|
parse_path("84'/17'/0'/0/0"),
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "grs1qw4teyraux2s77nhjdwh9ar8rl9dt7zww8r6lne"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_show_multisig_3(self):
|
||||||
|
self.setup_mnemonic_allallall()
|
||||||
|
nodes = [
|
||||||
|
btc.get_public_node(self.client, parse_path("999'/1'/%d'" % index))
|
||||||
|
for index in range(1, 4)
|
||||||
|
]
|
||||||
|
multisig1 = proto.MultisigRedeemScriptType(
|
||||||
|
nodes=[bip32.deserialize(n.xpub) for n in nodes],
|
||||||
|
address_n=[2, 0],
|
||||||
|
signatures=[b"", b"", b""],
|
||||||
|
m=2,
|
||||||
|
)
|
||||||
|
multisig2 = proto.MultisigRedeemScriptType(
|
||||||
|
nodes=[bip32.deserialize(n.xpub) for n in nodes],
|
||||||
|
address_n=[2, 1],
|
||||||
|
signatures=[b"", b"", b""],
|
||||||
|
m=2,
|
||||||
|
)
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("999'/1'/%d'/2/1" % i),
|
||||||
|
False,
|
||||||
|
multisig2,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "tb1qch62pf820spe9mlq49ns5uexfnl6jzcezp7d328fw58lj0rhlhasge9hzy"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client,
|
||||||
|
"Testnet",
|
||||||
|
parse_path("999'/1'/%d'/2/0" % i),
|
||||||
|
False,
|
||||||
|
multisig1,
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
)
|
||||||
|
== "tb1qr6xa5v60zyt3ry9nmfew2fk5g9y3gerkjeu6xxdz7qga5kknz2ssld9z2z"
|
||||||
|
)
|
@ -0,0 +1,84 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import btc, messages as proto
|
||||||
|
|
||||||
|
from ..support import ckd_public as bip32
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetaddressShow(TrezorTest):
|
||||||
|
def test_show(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [1], show_display=True)
|
||||||
|
== "1CK7SJdcb8z9HuvVft3D91HLpLC6KSsGb"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [2], show_display=True)
|
||||||
|
== "15AeAhtNJNKyowK8qPHwgpXkhsokzLtUpG"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_address(self.client, "Bitcoin", [3], show_display=True)
|
||||||
|
== "1CmzyJp9w3NafXMSEFH4SLYUPAVCSUrrJ5"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_show_multisig_3(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
node = bip32.deserialize(
|
||||||
|
"xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
multisig = proto.MultisigRedeemScriptType(
|
||||||
|
pubkeys=[
|
||||||
|
proto.HDNodePathType(node=node, address_n=[1]),
|
||||||
|
proto.HDNodePathType(node=node, address_n=[2]),
|
||||||
|
proto.HDNodePathType(node=node, address_n=[3]),
|
||||||
|
],
|
||||||
|
signatures=[b"", b"", b""],
|
||||||
|
m=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client, "Bitcoin", [i], show_display=True, multisig=multisig
|
||||||
|
)
|
||||||
|
== "3E7GDtuHqnqPmDgwH59pVC7AvySiSkbibz"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_show_multisig_15(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
node = bip32.deserialize(
|
||||||
|
"xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
|
||||||
|
pubs = []
|
||||||
|
for x in range(15):
|
||||||
|
pubs.append(proto.HDNodePathType(node=node, address_n=[x]))
|
||||||
|
|
||||||
|
multisig = proto.MultisigRedeemScriptType(
|
||||||
|
pubkeys=pubs, signatures=[b""] * 15, m=15
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(15):
|
||||||
|
assert (
|
||||||
|
btc.get_address(
|
||||||
|
self.client, "Bitcoin", [i], show_display=True, multisig=multisig
|
||||||
|
)
|
||||||
|
== "3QaKF8zobqcqY8aS6nxCD5ZYdiRfL3RCmU"
|
||||||
|
)
|
@ -0,0 +1,76 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import messages as proto, misc
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetECDHSessionKey(TrezorTest):
|
||||||
|
def test_ecdh(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
|
||||||
|
# URI : gpg://Satoshi Nakamoto <satoshi@bitcoin.org>
|
||||||
|
identity = proto.IdentityType(
|
||||||
|
proto="gpg",
|
||||||
|
user="",
|
||||||
|
host="Satoshi Nakamoto <satoshi@bitcoin.org>",
|
||||||
|
port="",
|
||||||
|
path="",
|
||||||
|
index=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
peer_public_key = bytes.fromhex(
|
||||||
|
"0407f2c6e5becf3213c1d07df0cfbe8e39f70a8c643df7575e5c56859ec52c45ca950499c019719dae0fda04248d851e52cf9d66eeb211d89a77be40de22b6c89d"
|
||||||
|
)
|
||||||
|
result = misc.get_ecdh_session_key(
|
||||||
|
self.client,
|
||||||
|
identity=identity,
|
||||||
|
peer_public_key=peer_public_key,
|
||||||
|
ecdsa_curve_name="secp256k1",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.session_key.hex()
|
||||||
|
== "0495e5d8c9e5cc09e7cf4908774f52decb381ce97f2fc9ba56e959c13f03f9f47a03dd151cbc908bc1db84d46e2c33e7bbb9daddc800f985244c924fd64adf6647"
|
||||||
|
)
|
||||||
|
|
||||||
|
peer_public_key = bytes.fromhex(
|
||||||
|
"04811a6c2bd2a547d0dd84747297fec47719e7c3f9b0024f027c2b237be99aac39a9230acbd163d0cb1524a0f5ea4bfed6058cec6f18368f72a12aa0c4d083ff64"
|
||||||
|
)
|
||||||
|
result = misc.get_ecdh_session_key(
|
||||||
|
self.client,
|
||||||
|
identity=identity,
|
||||||
|
peer_public_key=peer_public_key,
|
||||||
|
ecdsa_curve_name="nist256p1",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.session_key.hex()
|
||||||
|
== "046d1f5c48af2cf2c57076ac2c9d7808db2086f614cb7b8107119ff2c6270cd209749809efe0196f01a0cc633788cef1f4a2bd650c99570d06962f923fca6d8fdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
peer_public_key = bytes.fromhex(
|
||||||
|
"40a8cf4b6a64c4314e80f15a8ea55812bd735fbb365936a48b2d78807b575fa17a"
|
||||||
|
)
|
||||||
|
result = misc.get_ecdh_session_key(
|
||||||
|
self.client,
|
||||||
|
identity=identity,
|
||||||
|
peer_public_key=peer_public_key,
|
||||||
|
ecdsa_curve_name="curve25519",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.session_key.hex()
|
||||||
|
== "04e24516669e0b7d3d72e5129fddd07b6644c30915f5c8b7f1f62324afb3624311"
|
||||||
|
)
|
48
python/trezorlib/tests/device_tests/test_msg_getentropy.py
Normal file
48
python/trezorlib/tests/device_tests/test_msg_getentropy.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import messages as m, misc
|
||||||
|
|
||||||
|
ENTROPY_LENGTHS_POW2 = [2 ** l for l in range(10)]
|
||||||
|
ENTROPY_LENGTHS_POW2_1 = [2 ** l + 1 for l in range(10)]
|
||||||
|
|
||||||
|
ENTROPY_LENGTHS = ENTROPY_LENGTHS_POW2 + ENTROPY_LENGTHS_POW2_1
|
||||||
|
|
||||||
|
|
||||||
|
def entropy(data):
|
||||||
|
counts = {}
|
||||||
|
for c in data:
|
||||||
|
counts[c] = counts.get(c, 0) + 1
|
||||||
|
e = 0
|
||||||
|
for v in counts.values():
|
||||||
|
p = v / len(data)
|
||||||
|
e -= p * math.log(p, 256)
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("entropy_length", ENTROPY_LENGTHS)
|
||||||
|
def test_entropy(client, entropy_length):
|
||||||
|
with client:
|
||||||
|
client.set_expected_responses(
|
||||||
|
[m.ButtonRequest(code=m.ButtonRequestType.ProtectCall), m.Entropy()]
|
||||||
|
)
|
||||||
|
ent = misc.get_entropy(client, entropy_length)
|
||||||
|
assert len(ent) == entropy_length
|
||||||
|
print("{} bytes: entropy = {}".format(entropy_length, entropy(ent)))
|
168
python/trezorlib/tests/device_tests/test_msg_getpublickey.py
Normal file
168
python/trezorlib/tests/device_tests/test_msg_getpublickey.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
from trezorlib import btc, messages as proto
|
||||||
|
from trezorlib.tools import H_
|
||||||
|
|
||||||
|
from ..support import ckd_public as bip32
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetpublickey(TrezorTest):
|
||||||
|
def test_btc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
bip32.serialize(btc.get_public_node(self.client, []).node, 0x0488B21E)
|
||||||
|
== "xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(btc.get_public_node(self.client, [1]).node, 0x0488B21E)
|
||||||
|
== "xpub68zNxjsTrV8y9AadThLW7dTAqEpZ7xBLFSyJ3X9pjTv6Njg6kxgjXJkzxq8u3ttnjBw1jupQHMP3gpGZzZqd1eh5S4GjkaMhPR18vMyUi8N"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [1], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub68zNxjsTrV8y9AadThLW7dTAqEpZ7xBLFSyJ3X9pjTv6Njg6kxgjXJkzxq8u3ttnjBw1jupQHMP3gpGZzZqd1eh5S4GjkaMhPR18vMyUi8N"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [0, H_(1)]).node, 0x0488B21E
|
||||||
|
)
|
||||||
|
== "xpub6A3FoZqYXj1AbW4thRwBh26YwZWbmoyjTaZwwxJjY1oKUpefLepL3RFS9DHKQrjAfxDrzDepYMDZPqXN6upQm3bHQ9xaXD5a3mqni3goF4v"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [0, H_(1)], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub6A3FoZqYXj1AbW4thRwBh26YwZWbmoyjTaZwwxJjY1oKUpefLepL3RFS9DHKQrjAfxDrzDepYMDZPqXN6upQm3bHQ9xaXD5a3mqni3goF4v"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [H_(9), 0]).node, 0x0488B21E
|
||||||
|
)
|
||||||
|
== "xpub6A2h5mzLDfYginoD7q7wCWbq18wTbN9gducRr2w5NRTwdLeoT3cJSwefFqW7uXTpVFGtpUyDMBNYs3DNvvXx6NPjF9YEbUQrtxFSWnPtVrv"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [H_(9), 0], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub6A2h5mzLDfYginoD7q7wCWbq18wTbN9gducRr2w5NRTwdLeoT3cJSwefFqW7uXTpVFGtpUyDMBNYs3DNvvXx6NPjF9YEbUQrtxFSWnPtVrv"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [0, 9999999]).node, 0x0488B21E
|
||||||
|
)
|
||||||
|
== "xpub6A3FoZqQEK6iwLZ4HFkqSo5fb35BH4bpjC4SPZ63prfLdGYPwYxEuC6o91bUvFFdMzKWe5rs3axHRUjxJaSvBnKKFtnfLwDACRxPxabsv2r"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [0, 9999999], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub6A3FoZqQEK6iwLZ4HFkqSo5fb35BH4bpjC4SPZ63prfLdGYPwYxEuC6o91bUvFFdMzKWe5rs3axHRUjxJaSvBnKKFtnfLwDACRxPxabsv2r"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ltc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
bip32.serialize(btc.get_public_node(self.client, []).node, 0x019DA462)
|
||||||
|
== "Ltub2SSUS19CirucVPGDKDBatBDBEM2s9UbH66pBURfaKrMocCPLhQ7Z7hecy5VYLHA5fRdXwB2e61j2VJCNzVsqKTCVEU1vECjqi5EyczFX9xp"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [], coin_name="Litecoin").xpub
|
||||||
|
== "Ltub2SSUS19CirucVPGDKDBatBDBEM2s9UbH66pBURfaKrMocCPLhQ7Z7hecy5VYLHA5fRdXwB2e61j2VJCNzVsqKTCVEU1vECjqi5EyczFX9xp"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(btc.get_public_node(self.client, [1]).node, 0x019DA462)
|
||||||
|
== "Ltub2VRVRP5VjvSyPXra4BLVyVZPv397sjhUNjBGsbtw6xko77JuQyBULxFSKheviJJ3KQLbL3Cx8P2RnudguTw4raUVjCACRG7jsumUptYx55C"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [1], coin_name="Litecoin").xpub
|
||||||
|
== "Ltub2VRVRP5VjvSyPXra4BLVyVZPv397sjhUNjBGsbtw6xko77JuQyBULxFSKheviJJ3KQLbL3Cx8P2RnudguTw4raUVjCACRG7jsumUptYx55C"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [0, H_(1)]).node, 0x019DA462
|
||||||
|
)
|
||||||
|
== "Ltub2WUNGD3aRAKAqsLqHuwBYtCn2MqAXbVsarmvn33quWe2DCHTzfK4s4jsW5oM5G8RGAdSaM3NPNrwVvtV1ourbyNhhHr3BtqcYGc8caf5GoT"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [0, H_(1)], coin_name="Litecoin").xpub
|
||||||
|
== "Ltub2WUNGD3aRAKAqsLqHuwBYtCn2MqAXbVsarmvn33quWe2DCHTzfK4s4jsW5oM5G8RGAdSaM3NPNrwVvtV1ourbyNhhHr3BtqcYGc8caf5GoT"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [H_(9), 0]).node, 0x019DA462
|
||||||
|
)
|
||||||
|
== "Ltub2WToYRCN76rgyA59iK7w4Ni45wG2M9fpmBpQg7gBjvJeMiHc7473Gb96ci29Zvs55TgUQcMmCD1vy8aVqpdPwJB9YHRhGAAuPT1nRLLXmFu"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [H_(9), 0], coin_name="Litecoin").xpub
|
||||||
|
== "Ltub2WToYRCN76rgyA59iK7w4Ni45wG2M9fpmBpQg7gBjvJeMiHc7473Gb96ci29Zvs55TgUQcMmCD1vy8aVqpdPwJB9YHRhGAAuPT1nRLLXmFu"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [0, 9999999]).node, 0x019DA462
|
||||||
|
)
|
||||||
|
== "Ltub2WUNGD3S7kQjBhpzsjkqJfBtfqPk2r7xrUGRDdqACMW3MeBCbZSyiqbEVt7WaeesxCj6EDFQtcbfXa75DUYN2i6jZ2g81cyCgvijs9J2u2n"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [0, 9999999], coin_name="Litecoin").xpub
|
||||||
|
== "Ltub2WUNGD3S7kQjBhpzsjkqJfBtfqPk2r7xrUGRDdqACMW3MeBCbZSyiqbEVt7WaeesxCj6EDFQtcbfXa75DUYN2i6jZ2g81cyCgvijs9J2u2n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tbtc(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
bip32.serialize(
|
||||||
|
btc.get_public_node(self.client, [111, 42]).node, 0x043587CF
|
||||||
|
)
|
||||||
|
== "tpubDAgixSyai5PWbc8N1mBkHDR5nLgAnHFtY7r4y5EzxqAxrt9YUDpZL3kaRoHVvCfrcwNo31c2isBP2uTHcZxEosuKbyJhCAbrvGoPuLUZ7Mz"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [111, 42], coin_name="Testnet").xpub
|
||||||
|
== "tpubDAgixSyai5PWbc8N1mBkHDR5nLgAnHFtY7r4y5EzxqAxrt9YUDpZL3kaRoHVvCfrcwNo31c2isBP2uTHcZxEosuKbyJhCAbrvGoPuLUZ7Mz"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_type(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [], coin_name="Bitcoin").xpub
|
||||||
|
== "xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client,
|
||||||
|
[],
|
||||||
|
coin_name="Bitcoin",
|
||||||
|
script_type=proto.InputScriptType.SPENDADDRESS,
|
||||||
|
).xpub
|
||||||
|
== "xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client,
|
||||||
|
[],
|
||||||
|
coin_name="Bitcoin",
|
||||||
|
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
|
||||||
|
).xpub
|
||||||
|
== "ypub6QqdH2c5z7966KBPZ5yDEQCTKWrkLK4dsw8RRjpMLMtyvvZmJ3nNv7pKdQw6fnQkUrLm6XEeheSCGVSpoJCQGm6fofpt9RoHVJYH72ecmVm"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client,
|
||||||
|
[],
|
||||||
|
coin_name="Bitcoin",
|
||||||
|
script_type=proto.InputScriptType.SPENDWITNESS,
|
||||||
|
).xpub
|
||||||
|
== "zpub6jftahH18ngZwcNWPSkqSVHxVV1CGw48o3eeD8iEiNGrz2NzYhwwYBUTectgfh4ftVTZqzqDAJnk9n4PWzcR4znGg1XJjLcmm2bvVc3Honv"
|
||||||
|
)
|
@ -0,0 +1,84 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import btc
|
||||||
|
from trezorlib.tools import H_, CallException
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGetpublickeyCurve(TrezorTest):
|
||||||
|
def test_default_curve(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [H_(111), 42]).node.public_key.hex()
|
||||||
|
== "02e7fcec053f0df94d88c86447970743e8a1979d242d09338dcf8687a9966f7fbc"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(self.client, [H_(111), H_(42)]).node.public_key.hex()
|
||||||
|
== "03ce7b690969d773ba9ed212464eb2b534b87b9b8a9383300bddabe1f093f79220"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_secp256k1_curve(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), 42], ecdsa_curve_name="secp256k1"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "02e7fcec053f0df94d88c86447970743e8a1979d242d09338dcf8687a9966f7fbc"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), H_(42)], ecdsa_curve_name="secp256k1"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "03ce7b690969d773ba9ed212464eb2b534b87b9b8a9383300bddabe1f093f79220"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nist256p1_curve(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), 42], ecdsa_curve_name="nist256p1"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "02a9ce59b32bd64a70bc52aca96e5d09af65c6b9593ba2a60af8fccfe1437f2129"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), H_(42)], ecdsa_curve_name="nist256p1"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "026fe35d8afed67dbf0561a1d32922e8ad0cd0d86effbc82be970cbed7d9bab2c2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ed25519_curve(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
# ed25519 curve does not support public derivation, so test only private derivation paths
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), H_(42)], ecdsa_curve_name="ed25519"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "0069a14b478e508eab6e93303f4e6f5c50b8136627830f2ed5c3a835fc6c0ea2b7"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
btc.get_public_node(
|
||||||
|
self.client, [H_(111), H_(65535)], ecdsa_curve_name="ed25519"
|
||||||
|
).node.public_key.hex()
|
||||||
|
== "00514f73a05184458611b14c348fee4fd988d36cf3aee7207737861bac611de991"
|
||||||
|
)
|
||||||
|
# test failure when using public derivation
|
||||||
|
with pytest.raises(CallException):
|
||||||
|
btc.get_public_node(self.client, [H_(111), 42], ecdsa_curve_name="ed25519")
|
@ -0,0 +1,37 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import lisk
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
LISK_PATH = parse_path("m/44h/134h/0h/1h")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.lisk
|
||||||
|
class TestMsgLiskGetaddress(TrezorTest):
|
||||||
|
def test_lisk_getaddress(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
assert lisk.get_address(self.client, LISK_PATH[:2]) == "1431530009238518937L"
|
||||||
|
assert lisk.get_address(self.client, LISK_PATH[:3]) == "17563781916205589679L"
|
||||||
|
assert lisk.get_address(self.client, LISK_PATH) == "1874186517773691964L"
|
||||||
|
assert (
|
||||||
|
lisk.get_address(self.client, parse_path("m/44h/134h/999h/999h"))
|
||||||
|
== "16295203558710684671L"
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2018 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import lisk
|
||||||
|
from trezorlib.tools import parse_path
|
||||||
|
|
||||||
|
from .common import TrezorTest
|
||||||
|
|
||||||
|
LISK_PATH = parse_path("m/44h/134h/0h/0h")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.lisk
|
||||||
|
class TestMsgLiskGetPublicKey(TrezorTest):
|
||||||
|
def test_lisk_get_public_key(self):
|
||||||
|
self.setup_mnemonic_nopin_nopassphrase()
|
||||||
|
sig = lisk.get_public_key(self.client, LISK_PATH)
|
||||||
|
assert (
|
||||||
|
sig.public_key.hex()
|
||||||
|
== "eb56d7bbb5e8ea9269405f7a8527fe126023d1db2c973cfac6f760b60ae27294"
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user