diff --git a/.gitmodules b/.gitmodules index 5d8c985b1..4bb1d2151 100644 --- a/.gitmodules +++ b/.gitmodules @@ -45,3 +45,6 @@ path = legacy/vendor/QR-Code-generator url = https://github.com/nayuki/QR-Code-generator.git ignore = untracked +[submodule "python/vendor/trezor-common"] + path = python/vendor/trezor-common + url = https://github.com/trezor/trezor-common.git diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 000000000..d1f65b1c9 --- /dev/null +++ b/python/.gitignore @@ -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/* diff --git a/python/.travis.yml b/python/.travis.yml new file mode 100644 index 000000000..c7c009af1 --- /dev/null +++ b/python/.travis.yml @@ -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 diff --git a/python/AUTHORS b/python/AUTHORS new file mode 100644 index 000000000..03419f735 --- /dev/null +++ b/python/AUTHORS @@ -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 +Jan 'matejcik' Matějek +Jan Pochyla +Jochen Hoenicke +Karel Bílek +Marek Palatinus +mruddy +Pavol Rusnak +Peter van Mourik +Roman Zeyde +Saleem Rashid +Tomáš Sušánka +ZuluCrypto diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md new file mode 100644 index 000000000..f57193c44 --- /dev/null +++ b/python/CHANGELOG.md @@ -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 diff --git a/python/COPYING b/python/COPYING new file mode 100644 index 000000000..65c5ca88a --- /dev/null +++ b/python/COPYING @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 000000000..7a28c12ad --- /dev/null +++ b/python/MANIFEST.in @@ -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 diff --git a/python/Makefile b/python/Makefile new file mode 100644 index 000000000..ceda02221 --- /dev/null +++ b/python/Makefile @@ -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 diff --git a/python/README.md b/python/README.md new file mode 100644 index 000000000..dc54282c5 --- /dev/null +++ b/python/README.md @@ -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 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. diff --git a/python/bash_completion.d/trezorctl.sh b/python/bash_completion.d/trezorctl.sh new file mode 100644 index 000000000..581c15829 --- /dev/null +++ b/python/bash_completion.d/trezorctl.sh @@ -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 diff --git a/python/docs/EXAMPLES.rst b/python/docs/EXAMPLES.rst new file mode 100644 index 000000000..c6b22600f --- /dev/null +++ b/python/docs/EXAMPLES.rst @@ -0,0 +1,115 @@ +Examples demonstrating how to use trezorctl +=========================================== + +Show all available `options `_: + +.. 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 `_) 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 `_). 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 `_), `coinb.in `_ 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 `_). 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 `_ or later. diff --git a/python/docs/OPTIONS.rst b/python/docs/OPTIONS.rst new file mode 100644 index 000000000..170a48aa2 --- /dev/null +++ b/python/docs/OPTIONS.rst @@ -0,0 +1,83 @@ +Commandline options for trezorctl +================================= + +See `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. diff --git a/python/docs/README.rst b/python/docs/README.rst new file mode 100644 index 000000000..6153d74ba --- /dev/null +++ b/python/docs/README.rst @@ -0,0 +1,5 @@ +Documentation for trezorctl commandline client +============================================== + +* `EXAMPLES.rst `_ - Examples demonstrating how to use trezorctl +* `OPTIONS.rst `_ - Commandline options for trezorctl diff --git a/python/docs/sign_tx-coinb.in.png b/python/docs/sign_tx-coinb.in.png new file mode 100644 index 000000000..d10d68ca9 Binary files /dev/null and b/python/docs/sign_tx-coinb.in.png differ diff --git a/python/docs/sign_tx-electrum1.png b/python/docs/sign_tx-electrum1.png new file mode 100644 index 000000000..34d275482 Binary files /dev/null and b/python/docs/sign_tx-electrum1.png differ diff --git a/python/docs/sign_tx-electrum2.png b/python/docs/sign_tx-electrum2.png new file mode 100644 index 000000000..66f50d17f Binary files /dev/null and b/python/docs/sign_tx-electrum2.png differ diff --git a/python/docs/sign_tx-trezor.io.png b/python/docs/sign_tx-trezor.io.png new file mode 100644 index 000000000..a22d3ac14 Binary files /dev/null and b/python/docs/sign_tx-trezor.io.png differ diff --git a/python/helper-scripts/README.md b/python/helper-scripts/README.md new file mode 100644 index 000000000..5dadfcbe7 --- /dev/null +++ b/python/helper-scripts/README.md @@ -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 diff --git a/python/helper-scripts/bump-required-fw-versions.py b/python/helper-scripts/bump-required-fw-versions.py new file mode 100755 index 000000000..f968f1a53 --- /dev/null +++ b/python/helper-scripts/bump-required-fw-versions.py @@ -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) diff --git a/python/helper-scripts/linkify-changelog.py b/python/helper-scripts/linkify-changelog.py new file mode 100755 index 000000000..66b99b5b6 --- /dev/null +++ b/python/helper-scripts/linkify-changelog.py @@ -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") diff --git a/python/helper-scripts/relicence.py b/python/helper-scripts/relicence.py new file mode 100755 index 000000000..6c1064999 --- /dev/null +++ b/python/helper-scripts/relicence.py @@ -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 . + +""" + +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) diff --git a/python/requirements-dev.txt b/python/requirements-dev.txt new file mode 100644 index 000000000..811e30811 --- /dev/null +++ b/python/requirements-dev.txt @@ -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 diff --git a/python/requirements-optional.txt b/python/requirements-optional.txt new file mode 100644 index 000000000..63bf68191 --- /dev/null +++ b/python/requirements-optional.txt @@ -0,0 +1,3 @@ +hidapi >= 0.7.99.post20 +rlp >= 1.1.0 +web3 >= 4.8 diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 000000000..5eb37df69 --- /dev/null +++ b/python/requirements.txt @@ -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 diff --git a/python/setup.cfg b/python/setup.cfg new file mode 100644 index 000000000..779c34de0 --- /dev/null +++ b/python/setup.cfg @@ -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 diff --git a/python/setup.py b/python/setup.py new file mode 100755 index 000000000..ef5b23fbe --- /dev/null +++ b/python/setup.py @@ -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}, +) diff --git a/python/shell.nix b/python/shell.nix new file mode 100644 index 000000000..ed43d61b4 --- /dev/null +++ b/python/shell.nix @@ -0,0 +1,9 @@ +with import {}; + +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 ]; + } diff --git a/python/tools/deserialize_tx.py b/python/tools/deserialize_tx.py new file mode 100755 index 000000000..56ccb855a --- /dev/null +++ b/python/tools/deserialize_tx.py @@ -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)) diff --git a/python/tools/encfs_aes_getpass.py b/python/tools/encfs_aes_getpass.py new file mode 100755 index 000000000..0333d9742 --- /dev/null +++ b/python/tools/encfs_aes_getpass.py @@ -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("[-] \n") + continue + + if client.features.label: + sys.stderr.write("[%d] %s\n" % (i, client.features.label)) + else: + sys.stderr.write("[%d] \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() diff --git a/python/tools/helloworld.py b/python/tools/helloworld.py new file mode 100755 index 000000000..f4e9a9919 --- /dev/null +++ b/python/tools/helloworld.py @@ -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() diff --git a/python/tools/mem_flashblock.py b/python/tools/mem_flashblock.py new file mode 100755 index 000000000..cde9d6313 --- /dev/null +++ b/python/tools/mem_flashblock.py @@ -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() diff --git a/python/tools/mem_read.py b/python/tools/mem_read.py new file mode 100755 index 000000000..844118871 --- /dev/null +++ b/python/tools/mem_read.py @@ -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() diff --git a/python/tools/mem_write.py b/python/tools/mem_write.py new file mode 100755 index 000000000..4bdaa4678 --- /dev/null +++ b/python/tools/mem_write.py @@ -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() diff --git a/python/tools/mnemonic_check.py b/python/tools/mnemonic_check.py new file mode 100755 index 000000000..fe8bd6968 --- /dev/null +++ b/python/tools/mnemonic_check.py @@ -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() diff --git a/python/tools/pwd_reader.py b/python/tools/pwd_reader.py new file mode 100755 index 000000000..d962ceb81 --- /dev/null +++ b/python/tools/pwd_reader.py @@ -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() diff --git a/python/tools/rng_entropy_collector.py b/python/tools/rng_entropy_collector.py new file mode 100755 index 000000000..31ec8528b --- /dev/null +++ b/python/tools/rng_entropy_collector.py @@ -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() diff --git a/python/tools/trezor-otp.py b/python/tools/trezor-otp.py new file mode 100755 index 000000000..670823148 --- /dev/null +++ b/python/tools/trezor-otp.py @@ -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() diff --git a/python/tox.ini b/python/tox.ini new file mode 100644 index 000000000..9e6f10db3 --- /dev/null +++ b/python/tox.ini @@ -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 diff --git a/python/trezorctl b/python/trezorctl new file mode 100755 index 000000000..87b691133 --- /dev/null +++ b/python/trezorctl @@ -0,0 +1,1989 @@ +#!/usr/bin/env python3 + +# This file is part of the TREZOR project. +# +# Copyright (C) 2012-2017 Marek Palatinus +# Copyright (C) 2012-2017 Pavol Rusnak +# Copyright (C) 2016-2017 Jochen Hoenicke +# Copyright (C) 2017 mruddy +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 GNU Lesser General Public License +# along with this library. If not, see . + +import base64 +import json +import os +import re +import sys +from decimal import Decimal + +import click +import requests + +from trezorlib import ( + btc, + cardano, + coins, + cosi, + debuglink, + device, + ethereum, + exceptions, + firmware, + lisk, + log, + messages as proto, + misc, + monero, + nem, + ontology, + protobuf, + ripple, + stellar, + tezos, + tools, + ui, +) +from trezorlib.client import TrezorClient +from trezorlib.transport import enumerate_devices, get_transport + +try: + import rlp + import web3 + + ETHEREUM_SIGN_TX = True +except Exception: + ETHEREUM_SIGN_TX = False + + +class ChoiceType(click.Choice): + def __init__(self, typemap): + super(ChoiceType, self).__init__(typemap.keys()) + self.typemap = typemap + + def convert(self, value, param, ctx): + value = super(ChoiceType, self).convert(value, param, ctx) + return self.typemap[value] + + +CHOICE_PASSPHRASE_SOURCE_TYPE = ChoiceType( + { + "ask": proto.PassphraseSourceType.ASK, + "device": proto.PassphraseSourceType.DEVICE, + "host": proto.PassphraseSourceType.HOST, + } +) + + +CHOICE_DISPLAY_ROTATION_TYPE = ChoiceType( + {"north": 0, "east": 90, "south": 180, "west": 270} +) + + +CHOICE_RECOVERY_DEVICE_TYPE = ChoiceType( + { + "scrambled": proto.RecoveryDeviceType.ScrambledWords, + "matrix": proto.RecoveryDeviceType.Matrix, + } +) + +CHOICE_INPUT_SCRIPT_TYPE = ChoiceType( + { + "address": proto.InputScriptType.SPENDADDRESS, + "segwit": proto.InputScriptType.SPENDWITNESS, + "p2shsegwit": proto.InputScriptType.SPENDP2SHWITNESS, + } +) + +CHOICE_OUTPUT_SCRIPT_TYPE = ChoiceType( + { + "address": proto.OutputScriptType.PAYTOADDRESS, + "segwit": proto.OutputScriptType.PAYTOWITNESS, + "p2shsegwit": proto.OutputScriptType.PAYTOP2SHWITNESS, + } +) + + +class UnderscoreAgnosticGroup(click.Group): + """Command group that normalizes dashes and underscores. + + Click 7.0 silently switched all underscore_commands to dash-commands. + This implementation of `click.Group` responds to underscore_commands by invoking + the respective dash-command. + """ + + def get_command(self, ctx, cmd_name): + cmd = super().get_command(ctx, cmd_name) + if cmd is None: + cmd = super().get_command(ctx, cmd_name.replace("_", "-")) + return cmd + + +def enable_logging(): + log.enable_debug_output() + log.OMITTED_MESSAGES.add(proto.Features) + + +@click.command(cls=UnderscoreAgnosticGroup, context_settings={"max_content_width": 400}) +@click.option( + "-p", + "--path", + help="Select device by specific path.", + default=os.environ.get("TREZOR_PATH"), +) +@click.option("-v", "--verbose", is_flag=True, help="Show communication messages.") +@click.option( + "-j", "--json", "is_json", is_flag=True, help="Print result as JSON object" +) +@click.pass_context +def cli(ctx, path, verbose, is_json): + if verbose: + enable_logging() + + def get_device(): + try: + device = get_transport(path, prefix_search=False) + except Exception: + try: + device = get_transport(path, prefix_search=True) + except Exception: + click.echo("Failed to find a TREZOR device.") + if path is not None: + click.echo("Using path: {}".format(path)) + sys.exit(1) + return TrezorClient(transport=device, ui=ui.ClickUI()) + + ctx.obj = get_device + + +@cli.resultcallback() +def print_result(res, path, verbose, is_json): + if is_json: + if isinstance(res, protobuf.MessageType): + click.echo(json.dumps({res.__class__.__name__: res.__dict__})) + else: + click.echo(json.dumps(res, sort_keys=True, indent=4)) + else: + if isinstance(res, list): + for line in res: + click.echo(line) + elif isinstance(res, dict): + for k, v in res.items(): + if isinstance(v, dict): + for kk, vv in v.items(): + click.echo("%s.%s: %s" % (k, kk, vv)) + else: + click.echo("%s: %s" % (k, v)) + elif isinstance(res, protobuf.MessageType): + click.echo(protobuf.format_message(res)) + else: + click.echo(res) + + +# +# Common functions +# + + +@cli.command(name="list", help="List connected TREZOR devices.") +def ls(): + return enumerate_devices() + + +@cli.command(help="Show version of trezorctl/trezorlib.") +def version(): + from trezorlib import __version__ as VERSION + + return VERSION + + +# +# Basic device functions +# + + +@cli.command(help="Send ping message.") +@click.argument("message") +@click.option("-b", "--button-protection", is_flag=True) +@click.option("-p", "--pin-protection", is_flag=True) +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.pass_obj +def ping(connect, message, button_protection, pin_protection, passphrase_protection): + return connect().ping( + message, + button_protection=button_protection, + pin_protection=pin_protection, + passphrase_protection=passphrase_protection, + ) + + +@cli.command(help="Clear session (remove cached PIN, passphrase, etc.).") +@click.pass_obj +def clear_session(connect): + return connect().clear_session() + + +@cli.command(help="Get example entropy.") +@click.argument("size", type=int) +@click.pass_obj +def get_entropy(connect, size): + return misc.get_entropy(connect(), size).hex() + + +@cli.command(help="Retrieve device features and settings.") +@click.pass_obj +def get_features(connect): + return connect().features + + +# +# Device management functions +# + + +@cli.command(help="Change new PIN or remove existing.") +@click.option("-r", "--remove", is_flag=True) +@click.pass_obj +def change_pin(connect, remove): + return device.change_pin(connect(), remove) + + +@cli.command(help="Enable passphrase.") +@click.pass_obj +def enable_passphrase(connect): + return device.apply_settings(connect(), use_passphrase=True) + + +@cli.command(help="Disable passphrase.") +@click.pass_obj +def disable_passphrase(connect): + return device.apply_settings(connect(), use_passphrase=False) + + +@cli.command(help="Set new device label.") +@click.option("-l", "--label") +@click.pass_obj +def set_label(connect, label): + return device.apply_settings(connect(), label=label) + + +@cli.command() +@click.argument("source", type=CHOICE_PASSPHRASE_SOURCE_TYPE) +@click.pass_obj +def set_passphrase_source(connect, source): + """Set passphrase source. + + Configure how to enter passphrase on Trezor Model T. The options are: + + ask - always ask where to enter passphrase + device - always enter passphrase on device + host - always enter passphrase on host + """ + return device.apply_settings(connect(), passphrase_source=source) + + +@cli.command() +@click.argument("rotation", type=CHOICE_DISPLAY_ROTATION_TYPE) +@click.pass_obj +def set_display_rotation(connect, rotation): + """Set display rotation. + + Configure display rotation for Trezor Model T. The options are + north, east, south or west. + """ + return device.apply_settings(connect(), display_rotation=rotation) + + +@cli.command(help="Set auto-lock delay (in seconds).") +@click.argument("delay", type=str) +@click.pass_obj +def set_auto_lock_delay(connect, delay): + value, unit = delay[:-1], delay[-1:] + units = {"s": 1, "m": 60, "h": 3600} + if unit in units: + seconds = float(value) * units[unit] + else: + seconds = float(delay) # assume seconds if no unit is specified + return device.apply_settings(connect(), auto_lock_delay_ms=int(seconds * 1000)) + + +@cli.command(help="Set device flags.") +@click.argument("flags") +@click.pass_obj +def set_flags(connect, flags): + flags = flags.lower() + if flags.startswith("0b"): + flags = int(flags, 2) + elif flags.startswith("0x"): + flags = int(flags, 16) + else: + flags = int(flags) + return device.apply_flags(connect(), flags=flags) + + +@cli.command(help="Set new homescreen.") +@click.option("-f", "--filename", default=None) +@click.pass_obj +def set_homescreen(connect, filename): + if filename is None: + img = b"\x00" + elif filename.endswith(".toif"): + img = open(filename, "rb").read() + if img[:8] != b"TOIf\x90\x00\x90\x00": + raise tools.CallException( + proto.FailureType.DataError, + "File is not a TOIF file with size of 144x144", + ) + else: + from PIL import Image + + im = Image.open(filename) + if im.size != (128, 64): + raise tools.CallException( + proto.FailureType.DataError, "Wrong size of the image" + ) + im = im.convert("1") + pix = im.load() + img = bytearray(1024) + for j in range(64): + for i in range(128): + if pix[i, j]: + o = i + j * 128 + img[o // 8] |= 1 << (7 - o % 8) + img = bytes(img) + return device.apply_settings(connect(), homescreen=img) + + +@cli.command(help="Set U2F counter.") +@click.argument("counter", type=int) +@click.pass_obj +def set_u2f_counter(connect, counter): + return device.set_u2f_counter(connect(), counter) + + +@cli.command(help="Reset device to factory defaults and remove all private data.") +@click.option( + "-b", + "--bootloader", + help="Wipe device in bootloader mode. This also erases the firmware.", + is_flag=True, +) +@click.pass_obj +def wipe_device(connect, bootloader): + client = connect() + if bootloader: + if not client.features.bootloader_mode: + click.echo("Please switch your device to bootloader mode.") + sys.exit(1) + else: + click.echo("Wiping user data and firmware!") + else: + if client.features.bootloader_mode: + click.echo( + "Your device is in bootloader mode. This operation would also erase firmware." + ) + click.echo( + 'Specify "--bootloader" if that is what you want, or disconnect and reconnect device in normal mode.' + ) + click.echo("Aborting.") + sys.exit(1) + else: + click.echo("Wiping user data!") + + try: + return device.wipe(connect()) + except tools.CallException as e: + click.echo("Action failed: {} {}".format(*e.args)) + sys.exit(3) + + +@cli.command(help="Load custom configuration to the device.") +@click.option("-m", "--mnemonic") +@click.option("-e", "--expand", is_flag=True) +@click.option("-x", "--xprv") +@click.option("-p", "--pin", default="") +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-l", "--label", default="") +@click.option("-i", "--ignore-checksum", is_flag=True) +@click.option("-s", "--slip0014", is_flag=True) +@click.pass_obj +def load_device( + connect, + mnemonic, + expand, + xprv, + pin, + passphrase_protection, + label, + ignore_checksum, + slip0014, +): + if not mnemonic and not xprv and not slip0014: + raise tools.CallException( + proto.FailureType.DataError, "Please provide mnemonic or xprv" + ) + + client = connect() + if mnemonic: + return debuglink.load_device_by_mnemonic( + client, + mnemonic, + pin, + passphrase_protection, + label, + "english", + ignore_checksum, + expand, + ) + if xprv: + return debuglink.load_device_by_xprv( + client, xprv, pin, passphrase_protection, label, "english" + ) + if slip0014: + return debuglink.load_device_by_mnemonic( + client, " ".join(["all"] * 12), pin, passphrase_protection, "SLIP-0014" + ) + + +@cli.command(help="Start safe recovery workflow.") +@click.option("-w", "--words", type=click.Choice(["12", "18", "24"]), default="24") +@click.option("-e", "--expand", is_flag=True) +@click.option("-p", "--pin-protection", is_flag=True) +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-l", "--label") +@click.option( + "-t", "--type", "rec_type", type=CHOICE_RECOVERY_DEVICE_TYPE, default="scrambled" +) +@click.option("-d", "--dry-run", is_flag=True) +@click.pass_obj +def recovery_device( + connect, + words, + expand, + pin_protection, + passphrase_protection, + label, + rec_type, + dry_run, +): + if rec_type == proto.RecoveryDeviceType.ScrambledWords: + input_callback = ui.mnemonic_words(expand) + else: + input_callback = ui.matrix_words + click.echo(ui.RECOVERY_MATRIX_DESCRIPTION) + + return device.recover( + connect(), + word_count=int(words), + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label, + language="english", + input_callback=input_callback, + type=rec_type, + dry_run=dry_run, + ) + + +@cli.command(help="Perform device setup and generate new seed.") +@click.option("-e", "--show-entropy", is_flag=True) +@click.option("-t", "--strength", type=click.Choice(["128", "192", "256"])) +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-p", "--pin-protection", is_flag=True) +@click.option("-l", "--label") +@click.option("-u", "--u2f-counter", default=0) +@click.option("-s", "--skip-backup", is_flag=True) +@click.option("-n", "--no-backup", is_flag=True) +@click.pass_obj +def reset_device( + connect, + show_entropy, + strength, + passphrase_protection, + pin_protection, + label, + u2f_counter, + skip_backup, + no_backup, +): + if strength: + strength = int(strength) + return device.reset( + connect(), + display_random=show_entropy, + strength=strength, + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label, + language="english", + u2f_counter=u2f_counter, + skip_backup=skip_backup, + no_backup=no_backup, + ) + + +@cli.command(help="Perform device seed backup.") +@click.pass_obj +def backup_device(connect): + return device.backup(connect()) + + +# +# Firmware update +# + + +ALLOWED_FIRMWARE_FORMATS = { + 1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2), + 2: (firmware.FirmwareFormat.TREZOR_T,), +} + + +def _print_version(version): + vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version) + click.echo(vstr) + + +def validate_firmware(version, fw, expected_fingerprint=None): + if version == firmware.FirmwareFormat.TREZOR_ONE: + if fw.embedded_onev2: + click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)") + _print_version(fw.embedded_onev2.firmware_header.version) + else: + click.echo("Trezor One firmware image.") + elif version == firmware.FirmwareFormat.TREZOR_ONE_V2: + click.echo("Trezor One v2 firmware (1.8.0 or later)") + _print_version(fw.firmware_header.version) + elif version == firmware.FirmwareFormat.TREZOR_T: + click.echo("Trezor T firmware image.") + vendor = fw.vendor_header.vendor_string + vendor_version = "{major}.{minor}".format(**fw.vendor_header.version) + click.echo("Vendor header from {}, version {}".format(vendor, vendor_version)) + _print_version(fw.firmware_header.version) + + try: + firmware.validate(version, fw, allow_unsigned=False) + click.echo("Signatures are valid.") + except firmware.Unsigned: + if not click.confirm("No signatures found. Continue?", default=False): + sys.exit(1) + try: + firmware.validate(version, fw, allow_unsigned=True) + click.echo("Unsigned firmware looking OK.") + except firmware.FirmwareIntegrityError as e: + click.echo(e) + click.echo("Firmware validation failed, aborting.") + sys.exit(4) + except firmware.FirmwareIntegrityError as e: + click.echo(e) + click.echo("Firmware validation failed, aborting.") + sys.exit(4) + + fingerprint = firmware.digest(version, fw).hex() + click.echo("Firmware fingerprint: {}".format(fingerprint)) + if expected_fingerprint and fingerprint != expected_fingerprint: + click.echo("Expected fingerprint: {}".format(expected_fingerprint)) + click.echo("Fingerprints do not match, aborting.") + sys.exit(5) + + +def find_best_firmware_version(bootloader_version, requested_version=None): + url = "https://beta-wallet.trezor.io/data/firmware/{}/releases.json" + releases = requests.get(url.format(bootloader_version[0])).json() + if not releases: + raise click.ClickException("Failed to get list of releases") + + releases.sort(key=lambda r: r["version"], reverse=True) + + def version_str(version): + return ".".join(map(str, version)) + + want_version = requested_version + + if want_version is None: + want_version = releases[0]["version"] + click.echo("Best available version: {}".format(version_str(want_version))) + + confirm_different_version = False + while True: + want_version_str = version_str(want_version) + try: + release = next(r for r in releases if r["version"] == want_version) + except StopIteration: + click.echo("Version {} not found.".format(want_version_str)) + sys.exit(1) + + if ( + "min_bootloader_version" in release + and release["min_bootloader_version"] > bootloader_version + ): + need_version_str = version_str(release["min_firmware_version"]) + click.echo( + "Version {} is required before upgrading to {}.".format( + need_version_str, want_version_str + ) + ) + want_version = release["min_firmware_version"] + confirm_different_version = True + else: + break + + if confirm_different_version: + installing_different = "Installing version {} instead.".format(want_version_str) + if requested_version is None: + click.echo(installing_different) + else: + ok = click.confirm(installing_different + " Continue?", default=True) + if not ok: + sys.exit(1) + + url = "https://beta-wallet.trezor.io/" + release["url"] + if url.endswith(".hex"): + url = url[:-4] + + return url, release["fingerprint"] + + +@cli.command() +# fmt: off +@click.option("-f", "--filename") +@click.option("-u", "--url") +@click.option("-v", "--version") +@click.option("-s", "--skip-check", is_flag=True, help="Do not validate firmware integrity") +@click.option("-n", "--dry-run", is_flag=True, help="Perform all steps but do not actually upload the firmware") +@click.option("--raw", is_flag=True, help="Push raw data to Trezor") +@click.option("--fingerprint", help="Expected firmware fingerprint in hex") +@click.option("--skip-vendor-header", help="Skip vendor header validation on Trezor T") +# fmt: on +@click.pass_obj +def firmware_update( + connect, + filename, + url, + version, + skip_check, + fingerprint, + skip_vendor_header, + raw, + dry_run, +): + """Upload new firmware to device. + + Device must be in bootloader mode. + + You can specify a filename or URL from which the firmware can be downloaded. + You can also explicitly specify a firmware version that you want. + Otherwise, trezorctl will attempt to find latest available version + from wallet.trezor.io. + + If you provide a fingerprint via the --fingerprint option, it will be checked + against downloaded firmware fingerprint. Otherwise fingerprint is checked + against wallet.trezor.io information, if available. + + If you are customizing Model T bootloader and providing your own vendor header, + you can use --skip-vendor-header to ignore vendor header signatures. + """ + if sum(bool(x) for x in (filename, url, version)) > 1: + click.echo("You can use only one of: filename, url, version.") + sys.exit(1) + + client = connect() + if not dry_run and not client.features.bootloader_mode: + click.echo("Please switch your device to bootloader mode.") + sys.exit(1) + + f = client.features + bootloader_onev2 = f.major_version == 1 and f.minor_version >= 8 + + if filename: + data = open(filename, "rb").read() + else: + if not url: + bootloader_version = [f.major_version, f.minor_version, f.patch_version] + version_list = [int(x) for x in version.split(".")] if version else None + url, fp = find_best_firmware_version(bootloader_version, version_list) + if not fingerprint: + fingerprint = fp + + click.echo("Downloading from {}".format(url)) + r = requests.get(url) + data = r.content + + if not raw and not skip_check: + try: + version, fw = firmware.parse(data) + except Exception as e: + click.echo(e) + sys.exit(2) + + validate_firmware(version, fw, fingerprint) + if ( + bootloader_onev2 + and version == firmware.FirmwareFormat.TREZOR_ONE + and not fw.embedded_onev2 + ): + click.echo("Firmware is too old for your device. Aborting.") + sys.exit(3) + elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2: + click.echo("You need to upgrade to bootloader 1.8.0 first.") + sys.exit(3) + + if f.major_version not in ALLOWED_FIRMWARE_FORMATS: + click.echo("trezorctl doesn't know your device version. Aborting.") + sys.exit(3) + elif version not in ALLOWED_FIRMWARE_FORMATS[f.major_version]: + click.echo("Firmware does not match your device, aborting.") + sys.exit(3) + + if not raw: + # special handling for embedded-OneV2 format: + # for bootloader < 1.8, keep the embedding + # for bootloader 1.8.0 and up, strip the old OneV1 header + if bootloader_onev2 and data[:4] == b"TRZR" and data[256 : 256 + 4] == b"TRZF": + click.echo("Extracting embedded firmware image (fingerprint may change).") + data = data[256:] + + if dry_run: + click.echo("Dry run. Not uploading firmware to device.") + else: + try: + if f.major_version == 1 and f.firmware_present: + # Trezor One does not send ButtonRequest + click.echo("Please confirm action on your Trezor device") + return firmware.update(client, data) + except exceptions.Cancelled: + click.echo("Update aborted on device.") + except exceptions.TrezorException as e: + click.echo("Update failed: {}".format(e)) + sys.exit(3) + + +@cli.command(help="Perform a self-test.") +@click.pass_obj +def self_test(connect): + return debuglink.self_test(connect()) + + +@cli.command() +def usb_reset(): + """Perform USB reset on a stuck device. + + This can fix LIBUSB_ERROR_PIPE and similar errors when connecting to a device + in a messed state. + """ + from trezorlib.transport.webusb import WebUsbTransport + + WebUsbTransport.enumerate(usb_reset=True) + + +# +# Basic coin functions +# + + +@cli.command(help="Get address for specified path.") +@click.option("-c", "--coin", default="Bitcoin") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" +) +@click.option("-t", "--script-type", type=CHOICE_INPUT_SCRIPT_TYPE, default="address") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, coin, address, script_type, show_display): + client = connect() + address_n = tools.parse_path(address) + return btc.get_address( + client, coin, address_n, show_display, script_type=script_type + ) + + +@cli.command(help="Get public node of given path.") +@click.option("-c", "--coin", default="Bitcoin") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'") +@click.option("-e", "--curve") +@click.option("-t", "--script-type", type=CHOICE_INPUT_SCRIPT_TYPE, default="address") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_node(connect, coin, address, curve, script_type, show_display): + client = connect() + address_n = tools.parse_path(address) + result = btc.get_public_node( + client, + address_n, + ecdsa_curve_name=curve, + show_display=show_display, + coin_name=coin, + script_type=script_type, + ) + return { + "node": { + "depth": result.node.depth, + "fingerprint": "%08x" % result.node.fingerprint, + "child_num": result.node.child_num, + "chain_code": result.node.chain_code.hex(), + "public_key": result.node.public_key.hex(), + }, + "xpub": result.xpub, + } + + +# +# Signing options +# + + +@cli.command(help="Sign transaction.") +@click.option("-c", "--coin", default="Bitcoin") +@click.argument("json_file", type=click.File(), required=False) +@click.pass_obj +def sign_tx(connect, coin, json_file): + client = connect() + + # XXX this is the future code of this function + if json_file is not None: + data = json.load(json_file) + coin = data["coin_name"] + details = protobuf.dict_to_proto(proto.SignTx, data["details"]) + inputs = [protobuf.dict_to_proto(proto.TxInputType, i) for i in data["inputs"]] + outputs = [ + protobuf.dict_to_proto(proto.TxOutputType, output) + for output in data["outputs"] + ] + prev_txes = { + bytes.fromhex(txid): protobuf.dict_to_proto(proto.TransactionType, tx) + for txid, tx in data["prev_txes"].items() + } + + _, serialized_tx = btc.sign_tx( + client, coin, inputs, outputs, details, prev_txes + ) + + client.close() + + click.echo() + click.echo("Signed Transaction:") + click.echo(serialized_tx.hex()) + return + + # XXX ALL THE REST is legacy code and will be dropped + click.echo("Warning: interactive sign-tx mode is deprecated.", err=True) + click.echo( + "Instead, you should format your transaction data as JSON and " + "supply the file as an argument to sign-tx" + ) + if coin in coins.tx_api: + coin_data = coins.by_name[coin] + txapi = coins.tx_api[coin] + else: + click.echo('Coin "%s" is not recognized.' % coin, err=True) + click.echo( + "Supported coin types: %s" % ", ".join(coins.tx_api.keys()), err=True + ) + sys.exit(1) + + def default_script_type(address_n): + script_type = "address" + + if address_n is None: + pass + elif address_n[0] == tools.H_(49): + script_type = "p2shsegwit" + + return script_type + + def outpoint(s): + txid, vout = s.split(":") + return bytes.fromhex(txid), int(vout) + + inputs = [] + txes = {} + while True: + click.echo() + prev = click.prompt( + "Previous output to spend (txid:vout)", type=outpoint, default="" + ) + if not prev: + break + prev_hash, prev_index = prev + address_n = click.prompt("BIP-32 path to derive the key", type=tools.parse_path) + try: + tx = txapi[prev_hash] + txes[prev_hash] = tx + amount = tx.bin_outputs[prev_index].amount + click.echo("Prefilling input amount: {}".format(amount)) + except Exception as e: + print(e) + click.echo("Failed to fetch transation. This might bite you later.") + amount = click.prompt("Input amount (satoshis)", type=int, default=0) + sequence = click.prompt( + "Sequence Number to use (RBF opt-in enabled by default)", + type=int, + default=0xFFFFFFFD, + ) + script_type = click.prompt( + "Input type", + type=CHOICE_INPUT_SCRIPT_TYPE, + default=default_script_type(address_n), + ) + script_type = ( + script_type + if isinstance(script_type, int) + else CHOICE_INPUT_SCRIPT_TYPE.typemap[script_type] + ) + + new_input = proto.TxInputType( + address_n=address_n, + prev_hash=prev_hash, + prev_index=prev_index, + amount=amount, + script_type=script_type, + sequence=sequence, + ) + if coin_data["bip115"]: + prev_output = txapi.get_tx(prev_hash.hex()).bin_outputs[prev_index] + new_input.prev_block_hash_bip115 = prev_output.block_hash + new_input.prev_block_height_bip115 = prev_output.block_height + + inputs.append(new_input) + + if coin_data["bip115"]: + current_block_height = txapi.current_height() + # Zencash recommendation for the better protection + block_height = current_block_height - 300 + block_hash = txapi.get_block_hash(block_height) + # Blockhash passed in reverse order + block_hash = block_hash[::-1] + else: + block_height = None + block_hash = None + + outputs = [] + while True: + click.echo() + address = click.prompt("Output address (for non-change output)", default="") + if address: + address_n = None + else: + address = None + address_n = click.prompt( + "BIP-32 path (for change output)", type=tools.parse_path, default="" + ) + if not address_n: + break + amount = click.prompt("Amount to spend (satoshis)", type=int) + script_type = click.prompt( + "Output type", + type=CHOICE_OUTPUT_SCRIPT_TYPE, + default=default_script_type(address_n), + ) + script_type = ( + script_type + if isinstance(script_type, int) + else CHOICE_OUTPUT_SCRIPT_TYPE.typemap[script_type] + ) + outputs.append( + proto.TxOutputType( + address_n=address_n, + address=address, + amount=amount, + script_type=script_type, + block_hash_bip115=block_hash, + block_height_bip115=block_height, + ) + ) + + signtx = proto.SignTx() + signtx.version = click.prompt("Transaction version", type=int, default=2) + signtx.lock_time = click.prompt("Transaction locktime", type=int, default=0) + if coin == "Capricoin": + signtx.timestamp = click.prompt("Transaction timestamp", type=int) + + _, serialized_tx = btc.sign_tx( + client, coin, inputs, outputs, details=signtx, prev_txes=txes + ) + + client.close() + + click.echo() + click.echo("Signed Transaction:") + click.echo(serialized_tx.hex()) + click.echo() + click.echo("Use the following form to broadcast it to the network:") + click.echo(txapi.pushtx_url) + + +# +# Message functions +# + + +@cli.command(help="Sign message using address of given path.") +@click.option("-c", "--coin", default="Bitcoin") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" +) +@click.option( + "-t", + "--script-type", + type=click.Choice(["address", "segwit", "p2shsegwit"]), + default="address", +) +@click.argument("message") +@click.pass_obj +def sign_message(connect, coin, address, message, script_type): + client = connect() + address_n = tools.parse_path(address) + typemap = { + "address": proto.InputScriptType.SPENDADDRESS, + "segwit": proto.InputScriptType.SPENDWITNESS, + "p2shsegwit": proto.InputScriptType.SPENDP2SHWITNESS, + } + script_type = typemap[script_type] + res = btc.sign_message(client, coin, address_n, message, script_type) + return { + "message": message, + "address": res.address, + "signature": base64.b64encode(res.signature), + } + + +@cli.command(help="Verify message.") +@click.option("-c", "--coin", default="Bitcoin") +@click.argument("address") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def verify_message(connect, coin, address, signature, message): + signature = base64.b64decode(signature) + return btc.verify_message(connect(), coin, address, signature, message) + + +@cli.command(help="Sign message with Ethereum address.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" +) +@click.argument("message") +@click.pass_obj +def ethereum_sign_message(connect, address, message): + client = connect() + address_n = tools.parse_path(address) + ret = ethereum.sign_message(client, address_n, message) + output = { + "message": message, + "address": ret.address, + "signature": "0x%s" % ret.signature.hex(), + } + return output + + +def ethereum_decode_hex(value): + if value.startswith("0x") or value.startswith("0X"): + return bytes.fromhex(value[2:]) + else: + return bytes.fromhex(value) + + +@cli.command(help="Verify message signed with Ethereum address.") +@click.argument("address") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def ethereum_verify_message(connect, address, signature, message): + signature = ethereum_decode_hex(signature) + return ethereum.verify_message(connect(), address, signature, message) + + +@cli.command(help="Encrypt value by given key and path.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") +@click.argument("key") +@click.argument("value") +@click.pass_obj +def encrypt_keyvalue(connect, address, key, value): + client = connect() + address_n = tools.parse_path(address) + res = misc.encrypt_keyvalue(client, address_n, key, value.encode()) + return res.hex() + + +@cli.command(help="Decrypt value by given key and path.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") +@click.argument("key") +@click.argument("value") +@click.pass_obj +def decrypt_keyvalue(connect, address, key, value): + client = connect() + address_n = tools.parse_path(address) + return misc.decrypt_keyvalue(client, address_n, key, bytes.fromhex(value)) + + +# @cli.command(help='Encrypt message.') +# @click.option('-c', '--coin', default='Bitcoin') +# @click.option('-d', '--display-only', is_flag=True) +# @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") +# @click.argument('pubkey') +# @click.argument('message') +# @click.pass_obj +# def encrypt_message(connect, coin, display_only, address, pubkey, message): +# client = connect() +# pubkey = bytes.fromhex(pubkey) +# address_n = tools.parse_path(address) +# res = client.encrypt_message(pubkey, message, display_only, coin, address_n) +# return { +# 'nonce': res.nonce.hex(), +# 'message': res.message.hex(), +# 'hmac': res.hmac.hex(), +# 'payload': base64.b64encode(res.nonce + res.message + res.hmac), +# } + + +# @cli.command(help='Decrypt message.') +# @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") +# @click.argument('payload') +# @click.pass_obj +# def decrypt_message(connect, address, payload): +# client = connect() +# address_n = tools.parse_path(address) +# payload = base64.b64decode(payload) +# nonce, message, msg_hmac = payload[:33], payload[33:-8], payload[-8:] +# return client.decrypt_message(address_n, nonce, message, msg_hmac) + + +# +# Ethereum functions +# + + +@cli.command(help="Get Ethereum address in hex encoding.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def ethereum_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return ethereum.get_address(client, address_n, show_display) + + +@cli.command(help="Get Ethereum public node of given path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def ethereum_get_public_node(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + result = ethereum.get_public_node(client, address_n, show_display=show_display) + return { + "node": { + "depth": result.node.depth, + "fingerprint": "%08x" % result.node.fingerprint, + "child_num": result.node.child_num, + "chain_code": result.node.chain_code.hex(), + "public_key": result.node.public_key.hex(), + }, + "xpub": result.xpub, + } + + +# fmt: off +ETHER_UNITS = { + 'wei': 1, + 'kwei': 1000, + 'babbage': 1000, + 'femtoether': 1000, + 'mwei': 1000000, + 'lovelace': 1000000, + 'picoether': 1000000, + 'gwei': 1000000000, + 'shannon': 1000000000, + 'nanoether': 1000000000, + 'nano': 1000000000, + 'szabo': 1000000000000, + 'microether': 1000000000000, + 'micro': 1000000000000, + 'finney': 1000000000000000, + 'milliether': 1000000000000000, + 'milli': 1000000000000000, + 'ether': 1000000000000000000, + 'eth': 1000000000000000000, +} +# fmt: on + + +def ethereum_amount_to_int(ctx, param, value): + if value is None: + return None + if value.isdigit(): + return int(value) + try: + number, unit = re.match(r"^(\d+(?:.\d+)?)([a-z]+)", value).groups() + scale = ETHER_UNITS[unit] + decoded_number = Decimal(number) + return int(decoded_number * scale) + + except Exception: + import traceback + + traceback.print_exc() + raise click.BadParameter("Amount not understood") + + +def ethereum_list_units(ctx, param, value): + if not value or ctx.resilient_parsing: + return + maxlen = max(len(k) for k in ETHER_UNITS.keys()) + 1 + for unit, scale in ETHER_UNITS.items(): + click.echo("{:{maxlen}}: {}".format(unit, scale, maxlen=maxlen)) + ctx.exit() + + +def ethereum_erc20_contract(w3, token_address, to_address, amount): + min_abi = [ + { + "name": "transfer", + "type": "function", + "constant": False, + "inputs": [ + {"name": "_to", "type": "address"}, + {"name": "_value", "type": "uint256"}, + ], + "outputs": [{"name": "", "type": "bool"}], + } + ] + contract = w3.eth.contract(address=token_address, abi=min_abi) + return contract.encodeABI("transfer", [to_address, amount]) + + +@cli.command() +@click.option( + "-c", "--chain-id", type=int, default=1, help="EIP-155 chain id (replay protection)" +) +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to source address, e.g., m/44'/60'/0'/0/0", +) +@click.option( + "-g", "--gas-limit", type=int, help="Gas limit (required for offline signing)" +) +@click.option( + "-t", + "--gas-price", + help="Gas price (required for offline signing)", + callback=ethereum_amount_to_int, +) +@click.option( + "-i", "--nonce", type=int, help="Transaction counter (required for offline signing)" +) +@click.option("-d", "--data", help="Data as hex string, e.g. 0x12345678") +@click.option("-p", "--publish", is_flag=True, help="Publish transaction via RPC") +@click.option("-x", "--tx-type", type=int, help="TX type (used only for Wanchain)") +@click.option("-t", "--token", help="ERC20 token address") +@click.option( + "--list-units", + is_flag=True, + help="List known currency units and exit.", + is_eager=True, + callback=ethereum_list_units, + expose_value=False, +) +@click.argument("to_address") +@click.argument("amount", callback=ethereum_amount_to_int) +@click.pass_obj +def ethereum_sign_tx( + connect, + chain_id, + address, + amount, + gas_limit, + gas_price, + nonce, + data, + publish, + to_address, + tx_type, + token, +): + """Sign (and optionally publish) Ethereum transaction. + + Use TO_ADDRESS as destination address, or set to "" for contract creation. + + Specify a contract address with the --token option to send an ERC20 token. + + You can specify AMOUNT and gas price either as a number of wei, + or you can use a unit suffix. + + Use the --list-units option to show all known currency units. + ERC20 token amounts are specified in eth/wei, custom units are not supported. + + If any of gas price, gas limit and nonce is not specified, this command will + try to connect to an ethereum node and auto-fill these values. You can configure + the connection with WEB3_PROVIDER_URI environment variable. + """ + if not ETHEREUM_SIGN_TX: + click.echo("Ethereum requirements not installed.") + click.echo("Please run:") + click.echo() + click.echo(" pip install web3 rlp") + sys.exit(1) + + w3 = web3.Web3() + if ( + gas_price is None or gas_limit is None or nonce is None or publish + ) and not w3.isConnected(): + click.echo("Failed to connect to Ethereum node.") + click.echo( + "If you want to sign offline, make sure you provide --gas-price, " + "--gas-limit and --nonce arguments" + ) + sys.exit(1) + + if data is not None and token is not None: + click.echo("Can't send tokens and custom data at the same time") + sys.exit(1) + + client = connect() + address_n = tools.parse_path(address) + from_address = ethereum.get_address(client, address_n) + + if token: + data = ethereum_erc20_contract(w3, token, to_address, amount) + to_address = token + amount = 0 + + if data: + data = ethereum_decode_hex(data) + else: + data = b"" + + if gas_price is None: + gas_price = w3.eth.gasPrice + + if gas_limit is None: + gas_limit = w3.eth.estimateGas( + { + "to": to_address, + "from": from_address, + "value": amount, + "data": "0x%s" % data.hex(), + } + ) + + if nonce is None: + nonce = w3.eth.getTransactionCount(from_address) + + sig = ethereum.sign_tx( + client, + n=address_n, + tx_type=tx_type, + nonce=nonce, + gas_price=gas_price, + gas_limit=gas_limit, + to=to_address, + value=amount, + data=data, + chain_id=chain_id, + ) + + to = ethereum_decode_hex(to_address) + if tx_type is None: + transaction = rlp.encode((nonce, gas_price, gas_limit, to, amount, data) + sig) + else: + transaction = rlp.encode( + (tx_type, nonce, gas_price, gas_limit, to, amount, data) + sig + ) + tx_hex = "0x%s" % transaction.hex() + + if publish: + tx_hash = w3.eth.sendRawTransaction(tx_hex).hex() + return "Transaction published with ID: %s" % tx_hash + else: + return "Signed raw transaction:\n%s" % tx_hex + + +# +# ADA functions +# + + +@cli.command(help="Sign Cardano transaction.") +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.option("-N", "--network", type=int, default=1) +@click.pass_obj +def cardano_sign_tx(connect, file, network): + client = connect() + + transaction = json.load(file) + + inputs = [cardano.create_input(input) for input in transaction["inputs"]] + outputs = [cardano.create_output(output) for output in transaction["outputs"]] + transactions = transaction["transactions"] + + signed_transaction = cardano.sign_tx(client, inputs, outputs, transactions, network) + + return { + "tx_hash": signed_transaction.tx_hash.hex(), + "tx_body": signed_transaction.tx_body.hex(), + } + + +@cli.command(help="Get Cardano address.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def cardano_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_address(client, address_n, show_display) + + +@cli.command(help="Get Cardano public key.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +) +@click.pass_obj +def cardano_get_public_key(connect, address): + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_public_key(client, address_n) + + +# +# NEM functions +# + + +@cli.command(help="Get NEM address for specified path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/43'/0/0" +) +@click.option("-N", "--network", type=int, default=0x68) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def nem_get_address(connect, address, network, show_display): + client = connect() + address_n = tools.parse_path(address) + return nem.get_address(client, address_n, network, show_display) + + +@cli.command(help="Sign (and optionally broadcast) NEM transaction.") +@click.option("-n", "--address", help="BIP-32 path to signing key") +@click.option( + "-f", + "--file", + type=click.File("r"), + default="-", + help="Transaction in NIS (RequestPrepareAnnounce) format", +) +@click.option("-b", "--broadcast", help="NIS to announce transaction to") +@click.pass_obj +def nem_sign_tx(connect, address, file, broadcast): + client = connect() + address_n = tools.parse_path(address) + transaction = nem.sign_tx(client, address_n, json.load(file)) + + payload = {"data": transaction.data.hex(), "signature": transaction.signature.hex()} + + if broadcast: + return requests.post( + "{}/transaction/announce".format(broadcast), json=payload + ).json() + else: + return payload + + +# +# Lisk functions +# + + +@cli.command(help="Get Lisk address for specified path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def lisk_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return lisk.get_address(client, address_n, show_display) + + +@cli.command(help="Get Lisk public key for specified path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def lisk_get_public_key(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + res = lisk.get_public_key(client, address_n, show_display) + output = {"public_key": res.public_key.hex()} + return output + + +@cli.command(help="Sign Lisk transaction.") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to signing key, e.g. m/44'/134'/0'/0'", +) +@click.option( + "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" +) +# @click.option('-b', '--broadcast', help='Broadcast Lisk transaction') +@click.pass_obj +def lisk_sign_tx(connect, address, file): + client = connect() + address_n = tools.parse_path(address) + transaction = lisk.sign_tx(client, address_n, json.load(file)) + + payload = {"signature": transaction.signature.hex()} + + return payload + + +@cli.command(help="Sign message with Lisk address.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" +) +@click.argument("message") +@click.pass_obj +def lisk_sign_message(connect, address, message): + client = connect() + address_n = client.expand_path(address) + res = lisk.sign_message(client, address_n, message) + output = { + "message": message, + "public_key": res.public_key.hex(), + "signature": res.signature.hex(), + } + return output + + +@cli.command(help="Verify message signed with Lisk address.") +@click.argument("pubkey") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def lisk_verify_message(connect, pubkey, signature, message): + signature = bytes.fromhex(signature) + pubkey = bytes.fromhex(pubkey) + return lisk.verify_message(connect(), pubkey, signature, message) + + +# +# Monero functions +# + + +@cli.command(help="Get Monero address for specified path.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/128'/0'") +@click.option("-d", "--show-display", is_flag=True) +@click.option( + "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" +) +@click.pass_obj +def monero_get_address(connect, address, show_display, network_type): + client = connect() + address_n = tools.parse_path(address) + network_type = int(network_type) + return monero.get_address(client, address_n, show_display, network_type) + + +@cli.command(help="Get Monero watch key for specified path.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/128'/0'") +@click.option( + "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" +) +@click.pass_obj +def monero_get_watch_key(connect, address, network_type): + client = connect() + address_n = tools.parse_path(address) + network_type = int(network_type) + res = monero.get_watch_key(client, address_n, network_type) + output = {"address": res.address.decode(), "watch_key": res.watch_key.hex()} + return output + + +# +# CoSi functions +# + + +@cli.command(help="Ask device to commit to CoSi signing.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" +) +@click.argument("data") +@click.pass_obj +def cosi_commit(connect, address, data): + client = connect() + address_n = tools.parse_path(address) + return cosi.commit(client, address_n, bytes.fromhex(data)) + + +@cli.command(help="Ask device to sign using CoSi.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" +) +@click.argument("data") +@click.argument("global_commitment") +@click.argument("global_pubkey") +@click.pass_obj +def cosi_sign(connect, address, data, global_commitment, global_pubkey): + client = connect() + address_n = tools.parse_path(address) + return cosi.sign( + client, + address_n, + bytes.fromhex(data), + bytes.fromhex(global_commitment), + bytes.fromhex(global_pubkey), + ) + + +# +# Stellar functions +# +@cli.command(help="Get Stellar public address") +@click.option( + "-n", + "--address", + required=False, + help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", + default=stellar.DEFAULT_BIP32_PATH, +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def stellar_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return stellar.get_address(client, address_n, show_display) + + +@cli.command(help="Sign a base64-encoded transaction envelope") +@click.option( + "-n", + "--address", + required=False, + help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", + default=stellar.DEFAULT_BIP32_PATH, +) +@click.option( + "-n", + "--network-passphrase", + default=stellar.DEFAULT_NETWORK_PASSPHRASE, + required=False, + help="Network passphrase (blank for public network). Testnet is: 'Test SDF Network ; September 2015'", +) +@click.argument("b64envelope") +@click.pass_obj +def stellar_sign_transaction(connect, b64envelope, address, network_passphrase): + client = connect() + address_n = tools.parse_path(address) + tx, operations = stellar.parse_transaction_bytes(base64.b64decode(b64envelope)) + resp = stellar.sign_tx(client, tx, operations, address_n, network_passphrase) + + return base64.b64encode(resp.signature) + + +# +# Ripple functions +# +@cli.command(help="Get Ripple address") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def ripple_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return ripple.get_address(client, address_n, show_display) + + +@cli.command(help="Sign Ripple transaction") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0" +) +@click.option( + "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" +) +@click.pass_obj +def ripple_sign_tx(connect, address, file): + client = connect() + address_n = tools.parse_path(address) + msg = ripple.create_sign_tx_msg(json.load(file)) + + result = ripple.sign_tx(client, address_n, msg) + click.echo("Signature:") + click.echo(result.signature.hex()) + click.echo() + click.echo("Serialized tx including the signature:") + click.echo(result.serialized_tx.hex()) + + +# +# Tezos functions +# +@cli.command(help="Get Tezos address for specified path.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def tezos_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return tezos.get_address(client, address_n, show_display) + + +@cli.command(help="Get Tezos public key.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def tezos_get_public_key(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return tezos.get_public_key(client, address_n, show_display) + + +@cli.command(help="Sign Tezos transaction.") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") +@click.option( + "-f", + "--file", + type=click.File("r"), + default="-", + help="Transaction in JSON format (byte fields should be hexlified)", +) +@click.pass_obj +def tezos_sign_tx(connect, address, file): + client = connect() + address_n = tools.parse_path(address) + msg = protobuf.dict_to_proto(proto.TezosSignTx, json.load(file)) + return tezos.sign_tx(client, address_n, msg) + + +# +# Ontology functions +# + + +@cli.command(help="Get Ontology address for specified path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/888'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def ontology_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + return ontology.get_address(client, address_n, show_display) + + +@cli.command(help="Get Ontology public key for specified path.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/888'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def ontology_get_public_key(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + result = ontology.get_public_key(client, address_n, show_display) + + return result.public_key.hex() + + +@cli.command(help="Sign Ontology transfer.") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to signing key, e.g. m/44'/888'/0'/0/0", +) +@click.option( + "-tx", + "--transaction", + type=click.File("r"), + default="-", + help="Transaction in JSON format", +) +@click.option( + "-tr", + "--transfer", + type=click.File("r"), + default="-", + help="Transfer in JSON format", +) +@click.pass_obj +def ontology_sign_transfer(connect, address, transaction_f, transfer_f): + client = connect() + address_n = tools.parse_path(address) + transaction = protobuf.dict_to_proto( + proto.OntologyTransaction, json.load(transaction_f) + ) + transfer = protobuf.dict_to_proto(proto.OntologyTransfer, json.load(transfer_f)) + + result = ontology.sign_transfer(client, address_n, transaction, transfer) + + output = {"payload": result.payload.hex(), "signature": result.signature.hex()} + + return output + + +@cli.command(help="Sign Ontology withdraw Ong.") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to signing key, e.g. m/44'/888'/0'/0/0", +) +@click.option( + "-tx", + "--transaction", + type=click.File("r"), + default="-", + help="Transaction in JSON format", +) +@click.option( + "-wi", + "--withdraw_ong", + type=click.File("r"), + default="-", + help="Withdrawal in JSON format", +) +@click.pass_obj +def ontology_sign_withdraw_ong(connect, address, transaction_f, withdraw_ong_f): + client = connect() + address_n = tools.parse_path(address) + transaction = protobuf.dict_to_proto( + proto.OntologyTransaction, json.load(transaction_f) + ) + withdraw_ong = protobuf.dict_to_proto( + proto.OntologyWithdrawOng, json.load(withdraw_ong_f) + ) + + result = ontology.sign_withdrawal(client, address_n, transaction, withdraw_ong) + + output = {"payload": result.payload.hex(), "signature": result.signature.hex()} + + return output + + +@cli.command(help="Sign Ontology ONT ID Registration.") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to signing key, e.g. m/44'/888'/0'/0/0", +) +@click.option( + "-tx", + "--transaction", + type=click.File("r"), + default="-", + help="Transaction in JSON format", +) +@click.option( + "-re", + "--register", + type=click.File("r"), + default="-", + help="Register in JSON format", +) +@click.argument("transaction") +@click.argument("ont_id_register") +@click.pass_obj +def ontology_sign_ont_id_register(connect, address, transaction_f, ont_id_register_f): + client = connect() + address_n = tools.parse_path(address) + transaction = protobuf.dict_to_proto( + proto.OntologyTransaction, json.load(transaction_f) + ) + ont_id_register = protobuf.dict_to_proto( + proto.OntologyOntIdRegister, json.load(ont_id_register_f) + ) + + result = ontology.sign_register(client, address_n, transaction, ont_id_register) + + output = {"payload": result.payload.hex(), "signature": result.signature.hex()} + + return output + + +@cli.command(help="Sign Ontology ONT ID Attributes adding.") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to signing key, e.g. m/44'/888'/0'/0/0", +) +@click.option( + "-tx", + "--transaction", + type=click.File("r"), + default="-", + help="Transaction in JSON format", +) +@click.option( + "-aa", + "--add_attr", + type=click.File("r"), + default="-", + help="Add attributes in JSON format", +) +@click.pass_obj +def ontology_sign_ont_id_add_attributes( + connect, address, transaction_f, ont_id_add_attributes_f +): + client = connect() + address_n = tools.parse_path(address) + transaction = protobuf.dict_to_proto( + proto.OntologyTransaction, json.load(transaction_f) + ) + ont_id_add_attributes = protobuf.dict_to_proto( + proto.OntologyOntIdAddAttributes, json.load(ont_id_add_attributes_f) + ) + + result = ontology.sign_add_attr( + client, address_n, transaction, ont_id_add_attributes + ) + + output = {"payload": result.payload.hex(), "signature": result.signature.hex()} + + return output + + +# +# Main +# + + +if __name__ == "__main__": + cli() # pylint: disable=E1120 diff --git a/python/trezorlib/__init__.py b/python/trezorlib/__init__.py new file mode 100644 index 000000000..38bc6ef8f --- /dev/null +++ b/python/trezorlib/__init__.py @@ -0,0 +1,8 @@ +__version__ = "0.11.3" + +# fmt: off +MINIMUM_FIRMWARE_VERSION = { + "1": (1, 8, 0), + "T": (2, 1, 0), +} +# fmt: on diff --git a/python/trezorlib/_ed25519.py b/python/trezorlib/_ed25519.py new file mode 100644 index 000000000..f1959ac96 --- /dev/null +++ b/python/trezorlib/_ed25519.py @@ -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 +# 2013 by Donald Stufft +# 2013 by Alex Gaynor +# 2013 by Greg Price +# +# 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 +# . + +""" +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") diff --git a/python/trezorlib/btc.py b/python/trezorlib/btc.py new file mode 100644 index 000000000..c36052850 --- /dev/null +++ b/python/trezorlib/btc.py @@ -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 . + +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 diff --git a/python/trezorlib/cardano.py b/python/trezorlib/cardano.py new file mode 100644 index 000000000..dacf1957b --- /dev/null +++ b/python/trezorlib/cardano.py @@ -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 . + +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"]) + ) diff --git a/python/trezorlib/ckd_public.py b/python/trezorlib/ckd_public.py new file mode 100644 index 000000000..0bb1ea9c1 --- /dev/null +++ b/python/trezorlib/ckd_public.py @@ -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 . + +import warnings + +from .tests.support.ckd_public import * # noqa + +warnings.warn("ckd_public module is deprecated and will be removed", DeprecationWarning) diff --git a/python/trezorlib/client.py b/python/trezorlib/client.py new file mode 100644 index 000000000..c49ee0ea8 --- /dev/null +++ b/python/trezorlib/client.py @@ -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 . + +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 diff --git a/python/trezorlib/coins.py b/python/trezorlib/coins.py new file mode 100644 index 000000000..4a920e8d5 --- /dev/null +++ b/python/trezorlib/coins.py @@ -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 . + +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"] +} diff --git a/python/trezorlib/cosi.py b/python/trezorlib/cosi.py new file mode 100644 index 000000000..8bb4f4ea8 --- /dev/null +++ b/python/trezorlib/cosi.py @@ -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 . + +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, + ) + ) diff --git a/python/trezorlib/debuglink.py b/python/trezorlib/debuglink.py new file mode 100644 index 000000000..ae877bb26 --- /dev/null +++ b/python/trezorlib/debuglink.py @@ -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 . + +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" + ) + ) diff --git a/python/trezorlib/device.py b/python/trezorlib/device.py new file mode 100644 index 000000000..f8bf5dafd --- /dev/null +++ b/python/trezorlib/device.py @@ -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 . + +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 diff --git a/python/trezorlib/ethereum.py b/python/trezorlib/ethereum.py new file mode 100644 index 000000000..cad1e7e25 --- /dev/null +++ b/python/trezorlib/ethereum.py @@ -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 . + +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 diff --git a/python/trezorlib/exceptions.py b/python/trezorlib/exceptions.py new file mode 100644 index 000000000..f95271eef --- /dev/null +++ b/python/trezorlib/exceptions.py @@ -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 . + + +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 diff --git a/python/trezorlib/firmware.py b/python/trezorlib/firmware.py new file mode 100644 index 000000000..9e7bb21ee --- /dev/null +++ b/python/trezorlib/firmware.py @@ -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 . + +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) diff --git a/python/trezorlib/lisk.py b/python/trezorlib/lisk.py new file mode 100644 index 000000000..7e8ca3464 --- /dev/null +++ b/python/trezorlib/lisk.py @@ -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 . + +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)) diff --git a/python/trezorlib/log.py b/python/trezorlib/log.py new file mode 100644 index 000000000..50f778a12 --- /dev/null +++ b/python/trezorlib/log.py @@ -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 . + +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) diff --git a/python/trezorlib/mapping.py b/python/trezorlib/mapping.py new file mode 100644 index 000000000..11c94cb06 --- /dev/null +++ b/python/trezorlib/mapping.py @@ -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 . + +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() diff --git a/python/trezorlib/messages/.keep b/python/trezorlib/messages/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/python/trezorlib/misc.py b/python/trezorlib/misc.py new file mode 100644 index 000000000..11ef41565 --- /dev/null +++ b/python/trezorlib/misc.py @@ -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 . + +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, + ) + ) diff --git a/python/trezorlib/monero.py b/python/trezorlib/monero.py new file mode 100644 index 000000000..3c815f824 --- /dev/null +++ b/python/trezorlib/monero.py @@ -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 . + +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)) diff --git a/python/trezorlib/nem.py b/python/trezorlib/nem.py new file mode 100644 index 000000000..1d27e30fb --- /dev/null +++ b/python/trezorlib/nem.py @@ -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 . + +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) diff --git a/python/trezorlib/ontology.py b/python/trezorlib/ontology.py new file mode 100644 index 000000000..1913ed37e --- /dev/null +++ b/python/trezorlib/ontology.py @@ -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 . + +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 + ) + ) diff --git a/python/trezorlib/protobuf.py b/python/trezorlib/protobuf.py new file mode 100644 index 000000000..187a06786 --- /dev/null +++ b/python/trezorlib/protobuf.py @@ -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 . + +''' +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 diff --git a/python/trezorlib/qt/__init__.py b/python/trezorlib/qt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/trezorlib/qt/pinmatrix.py b/python/trezorlib/qt/pinmatrix.py new file mode 100644 index 000000000..a03e931b8 --- /dev/null +++ b/python/trezorlib/qt/pinmatrix.py @@ -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 . + +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_() diff --git a/python/trezorlib/ripple.py b/python/trezorlib/ripple.py new file mode 100644 index 000000000..9e8c6c70c --- /dev/null +++ b/python/trezorlib/ripple.py @@ -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 . + +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) diff --git a/python/trezorlib/stellar.py b/python/trezorlib/stellar.py new file mode 100644 index 000000000..2471704be --- /dev/null +++ b/python/trezorlib/stellar.py @@ -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 . + +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(" 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 diff --git a/python/trezorlib/tests/__init__.py b/python/trezorlib/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/trezorlib/tests/burn_tests/burntest_t1.py b/python/trezorlib/tests/burn_tests/burntest_t1.py new file mode 100755 index 000000000..c59782a2b --- /dev/null +++ b/python/trezorlib/tests/burn_tests/burntest_t1.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/burn_tests/burntest_t2.py b/python/trezorlib/tests/burn_tests/burntest_t2.py new file mode 100755 index 000000000..3ed9b87a0 --- /dev/null +++ b/python/trezorlib/tests/burn_tests/burntest_t2.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/.gitignore b/python/trezorlib/tests/device_tests/.gitignore new file mode 100644 index 000000000..b46fb42f3 --- /dev/null +++ b/python/trezorlib/tests/device_tests/.gitignore @@ -0,0 +1,2 @@ +*.out +*.err diff --git a/python/trezorlib/tests/device_tests/__init__.py b/python/trezorlib/tests/device_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/trezorlib/tests/device_tests/common.py b/python/trezorlib/tests/device_tests/common.py new file mode 100644 index 000000000..519a4e522 --- /dev/null +++ b/python/trezorlib/tests/device_tests/common.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/conftest.py b/python/trezorlib/tests/device_tests/conftest.py new file mode 100644 index 000000000..7e82c7fe0 --- /dev/null +++ b/python/trezorlib/tests/device_tests/conftest.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_basic.py b/python/trezorlib/tests/device_tests/test_basic.py new file mode 100644 index 000000000..707dc014b --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_basic.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_bip32_speed.py b/python/trezorlib/tests/device_tests/test_bip32_speed.py new file mode 100644 index 000000000..91072b238 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_bip32_speed.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_cancel.py b/python/trezorlib/tests/device_tests/test_cancel.py new file mode 100644 index 000000000..1259d67f9 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_cancel.py @@ -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 . + +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) diff --git a/python/trezorlib/tests/device_tests/test_cosi.py b/python/trezorlib/tests/device_tests/test_cosi.py new file mode 100644 index 000000000..c8117fd7e --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_cosi.py @@ -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 . + +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) diff --git a/python/trezorlib/tests/device_tests/test_debuglink.py b/python/trezorlib/tests/device_tests/test_debuglink.py new file mode 100644 index 000000000..4a91fdfaf --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_debuglink.py @@ -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 . + +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) diff --git a/python/trezorlib/tests/device_tests/test_msg_applysettings.py b/python/trezorlib/tests/device_tests/test_msg_applysettings.py new file mode 100644 index 000000000..4ae177632 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_applysettings.py @@ -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 . + +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) diff --git a/python/trezorlib/tests/device_tests/test_msg_cardano_get_address.py b/python/trezorlib/tests/device_tests/test_msg_cardano_get_address.py new file mode 100644 index 000000000..355135b62 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_cardano_get_address.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py b/python/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py new file mode 100644 index 000000000..32adf34b7 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py b/python/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py new file mode 100644 index 000000000..632ba5ad0 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_changepin.py b/python/trezorlib/tests/device_tests/test_msg_changepin.py new file mode 100644 index 000000000..4abcc609b --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_changepin.py @@ -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 . + +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) diff --git a/python/trezorlib/tests/device_tests/test_msg_cipherkeyvalue.py b/python/trezorlib/tests/device_tests/test_msg_cipherkeyvalue.py new file mode 100644 index 000000000..39d89361d --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_cipherkeyvalue.py @@ -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 . + +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") diff --git a/python/trezorlib/tests/device_tests/test_msg_clearsession.py b/python/trezorlib/tests/device_tests/test_msg_clearsession.py new file mode 100644 index 000000000..e0c11c186 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_clearsession.py @@ -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 . + +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" diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_getaddress.py new file mode 100644 index 000000000..8f6ff2708 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_getaddress.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_getpublickey.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_getpublickey.py new file mode 100644 index 000000000..19aae82ac --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_getpublickey.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_signmessage.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_signmessage.py new file mode 100644 index 000000000..5782c5eaa --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_signmessage.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx.py new file mode 100644 index 000000000..46451520a --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx.py @@ -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 . + +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, + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx_eip155.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx_eip155.py new file mode 100644 index 000000000..014d054af --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_signtx_eip155.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_ethereum_verifymessage.py b/python/trezorlib/tests/device_tests/test_msg_ethereum_verifymessage.py new file mode 100644 index 000000000..4c27bfa4e --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ethereum_verifymessage.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_getaddress.py new file mode 100644 index 000000000..80cd0e36d --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getaddress.py @@ -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 . + +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 diff --git a/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit.py b/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit.py new file mode 100644 index 000000000..d449180b1 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit_native.py b/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit_native.py new file mode 100644 index 000000000..d8b575905 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getaddress_segwit_native.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_getaddress_show.py b/python/trezorlib/tests/device_tests/test_msg_getaddress_show.py new file mode 100644 index 000000000..53729fb7f --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getaddress_show.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_getecdhsessionkey.py b/python/trezorlib/tests/device_tests/test_msg_getecdhsessionkey.py new file mode 100644 index 000000000..270b934bd --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getecdhsessionkey.py @@ -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 . + +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 + identity = proto.IdentityType( + proto="gpg", + user="", + host="Satoshi Nakamoto ", + 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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_getentropy.py b/python/trezorlib/tests/device_tests/test_msg_getentropy.py new file mode 100644 index 000000000..9711414be --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getentropy.py @@ -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 . + +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))) diff --git a/python/trezorlib/tests/device_tests/test_msg_getpublickey.py b/python/trezorlib/tests/device_tests/test_msg_getpublickey.py new file mode 100644 index 000000000..287d49632 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getpublickey.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_getpublickey_curve.py b/python/trezorlib/tests/device_tests/test_msg_getpublickey_curve.py new file mode 100644 index 000000000..7f350538f --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_getpublickey_curve.py @@ -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 . + +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") diff --git a/python/trezorlib/tests/device_tests/test_msg_lisk_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_lisk_getaddress.py new file mode 100644 index 000000000..571afbf91 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_lisk_getaddress.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_lisk_getpublickey.py b/python/trezorlib/tests/device_tests/test_msg_lisk_getpublickey.py new file mode 100644 index 000000000..aea8d7284 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_lisk_getpublickey.py @@ -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 . + +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" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_lisk_signmessage.py b/python/trezorlib/tests/device_tests/test_msg_lisk_signmessage.py new file mode 100644 index 000000000..32926b20f --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_lisk_signmessage.py @@ -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 . + +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 TestMsgLiskSignmessage(TrezorTest): + def test_sign(self): + self.setup_mnemonic_nopin_nopassphrase() + sig = lisk.sign_message( + self.client, LISK_PATH, "This is an example of a signed message." + ) + assert ( + sig.public_key.hex() + == "eb56d7bbb5e8ea9269405f7a8527fe126023d1db2c973cfac6f760b60ae27294" + ) + assert ( + sig.signature.hex() + == "7858ae7cd52ea6d4b17e800ca60144423db5560bfd618b663ffbf26ab66758563df45cbffae8463db22dc285dd94309083b8c807776085b97d05374d79867d05" + ) + + def test_sign_long(self): + self.setup_mnemonic_nopin_nopassphrase() + sig = lisk.sign_message(self.client, LISK_PATH, "VeryLongMessage!" * 64) + assert ( + sig.public_key.hex() + == "eb56d7bbb5e8ea9269405f7a8527fe126023d1db2c973cfac6f760b60ae27294" + ) + assert ( + sig.signature.hex() + == "19c26f4b6f2ecf2feef57d22237cf97eb7862fdc2fb8c303878843f5dd728191f7837cf8d0ed41f8e470b15181223a3a5131881add9c22b2453b01be4edef104" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_lisk_signtx.py b/python/trezorlib/tests/device_tests/test_msg_lisk_signtx.py new file mode 100644 index 000000000..f458019fe --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_lisk_signtx.py @@ -0,0 +1,245 @@ +# 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 . +import pytest + +from trezorlib import lisk, messages as proto +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.lisk +class TestMsgLiskSignTx(TrezorTest): + def test_lisk_sign_tx_send(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "f48532d43e8c5abadf50bb7b82098b31eec3e67747e5328c0675203e86441899c246fa3aea6fc91043209431ce710c5aa34aa234546b85b88299d5a379bff202" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "10000000", + "recipientId": "9971262264659915921L", + "timestamp": 57525937, + "type": 0, + "fee": "10000000", + "asset": {}, + }, + ) + + @pytest.mark.skip_t1 + def test_lisk_sign_tx_send_wrong_path(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest( + code=proto.ButtonRequestType.UnknownDerivationPath + ), + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "cdce9eba2ea8fa75f90fbc725f0d9de6152c7189a3044ab2fe307d9ff54754856e09125d7a15376eaf4bb5451b63881821948222ccd9ffb5da4d9b1aa8bd4904" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/123456'/123456'/123456'/123456'/123456'"), + { + "amount": "10000000", + "recipientId": "9971262264659915921L", + "timestamp": 57525937, + "type": 0, + "fee": "10000000", + "asset": {}, + }, + ) + + def test_lisk_sign_tx_send_with_data(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "4e83a651e82f2f787a71a5f44a2911dd0429ee4001b80c79fb7d174ea63ceeefdfba55aa3a9f31fa14b8325a39ad973dcd7eadbaa77b0447a9893f84b60f210e" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "10000000", + "recipientId": "9971262264659915921L", + "timestamp": 57525937, + "type": 0, + "fee": "20000000", + "asset": {"data": "Test data"}, + }, + ) + + def test_lisk_sign_tx_second_signature(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.PublicKey), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "e27d8997d0bdbc9ab4ad928fcf140edb25a217007987447270085c0872e4178c018847d1378a949ad2aa913692f10aeec340810fd9de02da9d4461c63b6b6c06" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "0", + "timestamp": 57525937, + "type": 1, + "fee": "500000000", + "asset": { + "signature": { + "publicKey": "5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09" + } + }, + }, + ) + + def test_lisk_sign_tx_delegate_registration(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "e9f68b9961198f4e0d33d6ae95cbd90ab243c2c1f9fcc51db54eb54cc1491db53d237131e12da9485bfbfbd02255c431d08095076f926060c434edb01cf25807" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "0", + "timestamp": 57525937, + "type": 2, + "fee": "2500000000", + "asset": {"delegate": {"username": "trezor_t"}}, + }, + ) + + def test_lisk_sign_tx_cast_votes(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "18d7cb27276a83178427aab2abcb5ee1c8ae9e8e2d1231585dcae7a83dd7d5167eea5baca890169bc80dcaf187320cab47c2f65a20c6483fede0f059919e4106" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "0", + "timestamp": 57525937, + "type": 3, + "fee": "100000000", + "asset": { + "votes": [ + "+b002f58531c074c7190714523eec08c48db8c7cfc0c943097db1a2e82ed87f84", + "-ec111c8ad482445cfe83d811a7edd1f1d2765079c99d7d958cca1354740b7614", + ] + }, + }, + ) + + def test_lisk_sign_tx_multisignature(self): + self.setup_mnemonic_allallall() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + proto.LiskSignedTx( + signature=bytes.fromhex( + "b84438ae3d419d270eacd0414fc8818d8f2c721602be54c3d705cf4cb3305de44e674f6dac9aac87379cce006cc97f2f635f296a48ab6a6adf62e2c11e08e409" + ) + ), + ] + ) + + lisk.sign_tx( + self.client, + parse_path("m/44'/134'/0'"), + { + "amount": "0", + "timestamp": 57525937, + "type": 4, + "fee": "1500000000", + "asset": { + "multisignature": { + "min": 2, + "lifetime": 5, + "keysgroup": [ + "+5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09", + "+922fbfdd596fa78269bbcadc67ec2a1cc15fc929a19c462169568d7a3df1a1aa", + ], + } + }, + }, + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_lisk_verifymessage.py b/python/trezorlib/tests/device_tests/test_msg_lisk_verifymessage.py new file mode 100644 index 000000000..dec148fee --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_lisk_verifymessage.py @@ -0,0 +1,66 @@ +# 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 . + +import pytest + +from trezorlib import lisk, messages as proto + +from .common import TrezorTest + + +@pytest.mark.lisk +class TestMsgLiskVerifymessage(TrezorTest): + def test_verify(self): + self.setup_mnemonic_nopin_nopassphrase() + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.Other), + proto.ButtonRequest(code=proto.ButtonRequestType.Other), + proto.Success(message="Message verified"), + ] + ) + lisk.verify_message( + self.client, + bytes.fromhex( + "eb56d7bbb5e8ea9269405f7a8527fe126023d1db2c973cfac6f760b60ae27294" + ), + bytes.fromhex( + "7858ae7cd52ea6d4b17e800ca60144423db5560bfd618b663ffbf26ab66758563df45cbffae8463db22dc285dd94309083b8c807776085b97d05374d79867d05" + ), + "This is an example of a signed message.", + ) + + def test_verify_long(self): + self.setup_mnemonic_nopin_nopassphrase() + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.Other), + proto.ButtonRequest(code=proto.ButtonRequestType.Other), + proto.Success(message="Message verified"), + ] + ) + lisk.verify_message( + self.client, + bytes.fromhex( + "8bca6b65a1a877767b746ea0b3c4310d404aa113df99c1b554e1802d70185ab5" + ), + bytes.fromhex( + "458ca5896d0934866992268f7509b5e954d568b1251e20c19bd3149ee3c86ffb5a44d1c2a0abbb99a3ab4767272dbb0e419b4579e890a24919ebbbe6cc0f970f" + ), + "VeryLongMessage!" * 64, + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_loaddevice.py b/python/trezorlib/tests/device_tests/test_msg_loaddevice.py new file mode 100644 index 000000000..ed178c146 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_loaddevice.py @@ -0,0 +1,120 @@ +# 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 . + +import pytest + +from trezorlib import btc, debuglink, device + +from .common import TrezorTest + + +@pytest.mark.skip_t2 +class TestDeviceLoad(TrezorTest): + def test_load_device_1(self): + self.setup_mnemonic_nopin_nopassphrase(lock=False) + state = self.client.debug.state() + assert state.mnemonic_secret == self.mnemonic12.encode() + assert state.pin is None + assert state.passphrase_protection is False + + address = btc.get_address(self.client, "Bitcoin", []) + assert address == "1EfKbQupktEMXf4gujJ9kCFo83k1iMqwqK" + + def test_load_device_2(self): + self.setup_mnemonic_pin_passphrase(lock=False) + self.client.set_passphrase("passphrase") + state = self.client.debug.state() + assert state.mnemonic_secret == self.mnemonic12.encode() + assert state.pin == self.pin4 + assert state.passphrase_protection is True + + address = btc.get_address(self.client, "Bitcoin", []) + assert address == "15fiTDFwZd2kauHYYseifGi9daH2wniDHH" + + def test_load_device_utf(self): + words_nfkd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a" + words_nfc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f" + words_nfkc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f" + words_nfd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a" + + passphrase_nfkd = ( + u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko" + ) + passphrase_nfc = ( + u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko" + ) + passphrase_nfkc = ( + u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko" + ) + passphrase_nfd = ( + u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko" + ) + + device.wipe(self.client) + debuglink.load_device_by_mnemonic( + self.client, + mnemonic=words_nfkd, + pin="", + passphrase_protection=True, + label="test", + language="english", + skip_checksum=True, + ) + self.client.set_passphrase(passphrase_nfkd) + address_nfkd = btc.get_address(self.client, "Bitcoin", []) + + device.wipe(self.client) + debuglink.load_device_by_mnemonic( + self.client, + mnemonic=words_nfc, + pin="", + passphrase_protection=True, + label="test", + language="english", + skip_checksum=True, + ) + self.client.set_passphrase(passphrase_nfc) + address_nfc = btc.get_address(self.client, "Bitcoin", []) + + device.wipe(self.client) + debuglink.load_device_by_mnemonic( + self.client, + mnemonic=words_nfkc, + pin="", + passphrase_protection=True, + label="test", + language="english", + skip_checksum=True, + ) + self.client.set_passphrase(passphrase_nfkc) + address_nfkc = btc.get_address(self.client, "Bitcoin", []) + + device.wipe(self.client) + debuglink.load_device_by_mnemonic( + self.client, + mnemonic=words_nfd, + pin="", + passphrase_protection=True, + label="test", + language="english", + skip_checksum=True, + ) + self.client.set_passphrase(passphrase_nfd) + address_nfd = btc.get_address(self.client, "Bitcoin", []) + + assert address_nfkd == address_nfc + assert address_nfkd == address_nfkc + assert address_nfkd == address_nfd diff --git a/python/trezorlib/tests/device_tests/test_msg_loaddevice_xprv.py b/python/trezorlib/tests/device_tests/test_msg_loaddevice_xprv.py new file mode 100644 index 000000000..13e2978f6 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_loaddevice_xprv.py @@ -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 . + +import pytest + +from trezorlib import btc, debuglink + +from .common import TrezorTest + + +@pytest.mark.skip_t2 +class TestDeviceLoadXprv(TrezorTest): + def test_load_device_xprv_1(self): + debuglink.load_device_by_xprv( + self.client, + xprv="xprv9s21ZrQH143K2JF8RafpqtKiTbsbaxEeUaMnNHsm5o6wCW3z8ySyH4UxFVSfZ8n7ESu7fgir8imbZKLYVBxFPND1pniTZ81vKfd45EHKX73", + pin="", + passphrase_protection=False, + label="test", + language="english", + ) + + passphrase_protection = self.client.debug.read_passphrase_protection() + assert passphrase_protection is False + + address = btc.get_address(self.client, "Bitcoin", []) + assert address == "128RdrAkJDmqasgvfRf6MC5VcX4HKqH4mR" + + def test_load_device_xprv_2(self): + debuglink.load_device_by_xprv( + self.client, + xprv="xprv9s21ZrQH143K2JF8RafpqtKiTbsbaxEeUaMnNHsm5o6wCW3z8ySyH4UxFVSfZ8n7ESu7fgir8imbZKLYVBxFPND1pniTZ81vKfd45EHKX73", + pin="", + passphrase_protection=True, + label="test", + language="english", + ) + + self.client.set_passphrase("passphrase") + + passphrase_protection = self.client.debug.read_passphrase_protection() + assert passphrase_protection is True + + address = btc.get_address(self.client, "Bitcoin", []) + assert address == "1CHUbFa4wTTPYgkYaw2LHSd5D4qJjMU8ri" diff --git a/python/trezorlib/tests/device_tests/test_msg_monero_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_monero_getaddress.py new file mode 100644 index 000000000..ff61d6323 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_monero_getaddress.py @@ -0,0 +1,41 @@ +# 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 . + +import pytest + +from trezorlib import monero +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.monero +@pytest.mark.skip_t1 +class TestMsgMoneroGetaddress(TrezorTest): + def test_monero_getaddress(self): + self.setup_mnemonic_nopin_nopassphrase() + assert ( + monero.get_address(self.client, parse_path("m/44h/128h/0h")) + == b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM" + ) + assert ( + monero.get_address(self.client, parse_path("m/44h/128h/1h")) + == b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie" + ) + assert ( + monero.get_address(self.client, parse_path("m/44h/128h/2h")) + == b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_monero_getwatchkey.py b/python/trezorlib/tests/device_tests/test_msg_monero_getwatchkey.py new file mode 100644 index 000000000..f6bd5ce1f --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_monero_getwatchkey.py @@ -0,0 +1,56 @@ +# 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 . + +import pytest + +from trezorlib import monero +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.monero +@pytest.mark.skip_t1 +class TestMsgMoneroGetwatchkey(TrezorTest): + def test_monero_getwatchkey(self): + self.setup_mnemonic_nopin_nopassphrase() + res = monero.get_watch_key(self.client, parse_path("m/44h/128h/0h")) + assert ( + res.address + == b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM" + ) + assert ( + res.watch_key.hex() + == "8722520a581e2a50cc1adab4a1692401effd37b0d63b9d9b60fd7f34ea2b950e" + ) + res = monero.get_watch_key(self.client, parse_path("m/44h/128h/1h")) + assert ( + res.address + == b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie" + ) + assert ( + res.watch_key.hex() + == "1f70b7d9e86c11b7a5bee883b75c43d6be189c8f812726ea1ecd94b06bb7db04" + ) + res = monero.get_watch_key(self.client, parse_path("m/44h/128h/2h")) + assert ( + res.address + == b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH" + ) + assert ( + res.watch_key.hex() + == "e0671fbed2c9231fe4f286962862813a4a4d153c793bf5d0e3742119723f3000" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_nem_getaddress.py new file mode 100644 index 000000000..3b92e84da --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_getaddress.py @@ -0,0 +1,36 @@ +# 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 . + +import pytest + +from trezorlib import nem +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.nem +class TestMsgNEMGetaddress(TrezorTest): + def test_nem_getaddress(self): + self.setup_mnemonic_nopin_nopassphrase() + assert ( + nem.get_address(self.client, parse_path("m/44'/1'/0'/0'/0'"), 0x68) + == "NB3JCHVARQNGDS3UVGAJPTFE22UQFGMCQGHUBWQN" + ) + assert ( + nem.get_address(self.client, parse_path("m/44'/1'/0'/0'/0'"), 0x98) + == "TB3JCHVARQNGDS3UVGAJPTFE22UQFGMCQHSBNBMF" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics.py b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics.py new file mode 100644 index 000000000..70985e0eb --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics.py @@ -0,0 +1,171 @@ +# 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 . + +import pytest + +from trezorlib import nem +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +# assertion data from T1 +@pytest.mark.nem +@pytest.mark.skip_t2 +class TestMsgNEMSignTxMosaics(TrezorTest): + def test_nem_signtx_mosaic_supply_change(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_SUPPLY_CHANGE, + "deadline": 74735615, + "message": {}, + "mosaicId": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "supplyType": 1, + "delta": 1, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + }, + ) + + assert ( + tx.data.hex() + == "02400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74041a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963010000000100000000000000" + ) + assert ( + tx.signature.hex() + == "928b03c4a69fff35ecf0912066ea705895b3028fad141197d7ea2b56f1eef2a2516455e6f35d318f6fa39e2bb40492ac4ae603260790f7ebc7ea69feb4ca4c0a" + ) + + def test_nem_signtx_mosaic_creation(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "levy": {}, + "properties": {}, + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + }, + ) + + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f7404c100000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000030160000000d000000696e697469616c537570706c7901000000301a0000000d000000737570706c794d757461626c650500000066616c7365190000000c0000007472616e7366657261626c650500000066616c7365000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "537adf4fd9bd5b46e204b2db0a435257a951ed26008305e0aa9e1201dafa4c306d7601a8dbacabf36b5137724386124958d53202015ab31fb3d0849dfed2df0e" + ) + + def test_nem_signtx_mosaic_creation_properties(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "levy": {}, + "properties": [ + {"name": "divisibility", "value": "4"}, + {"name": "initialSupply", "value": "200"}, + {"name": "supplyMutable", "value": "false"}, + {"name": "transferable", "value": "true"}, + ], + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + }, + ) + + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f7404c200000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000034180000000d000000696e697469616c537570706c79030000003230301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c650400000074727565000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "f17c859710060f2ea9a0ab740ef427431cf36bdc7d263570ca282bd66032e9f5737a921be9839429732e663be2bb74ccc16f34f5157ff2ef00a65796b54e800e" + ) + + def test_nem_signtx_mosaic_creation_levy(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "properties": [ + {"name": "divisibility", "value": "4"}, + {"name": "initialSupply", "value": "200"}, + {"name": "supplyMutable", "value": "false"}, + {"name": "transferable", "value": "true"}, + ], + "levy": { + "type": 1, + "fee": 2, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "mosaicId": {"namespaceId": "hellom", "name": "Hello mosaic"}, + }, + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + }, + ) + + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74041801000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000034180000000d000000696e697469616c537570706c79030000003230301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c65040000007472756556000000010000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a1a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f7361696302000000000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "b87aac1ddf146d35e6a7f3451f57e2fe504ac559031e010a51261257c37bd50fcfa7b2939dd7a3203b54c4807d458475182f5d3dc135ec0d1d4a9cd42159fd0a" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics_t2.py b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics_t2.py new file mode 100644 index 000000000..f204fc41a --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_mosaics_t2.py @@ -0,0 +1,206 @@ +# 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 . + +import time + +import pytest + +from trezorlib import messages as proto, nem +from trezorlib.messages import ButtonRequestType as B +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +# assertion data from T1 +@pytest.mark.nem +@pytest.mark.skip_t1 +class TestMsgNEMSignTxMosaics(TrezorTest): + def test_nem_signtx_mosaic_supply_change(self): + self.setup_mnemonic_nopin_nopassphrase() + + with self.client: + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_SUPPLY_CHANGE, + "deadline": 74735615, + "message": {}, + "mosaicId": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "supplyType": 1, + "delta": 1, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + }, + ) + + assert ( + tx.data.hex() + == "02400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74041a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963010000000100000000000000" + ) + assert ( + tx.signature.hex() + == "928b03c4a69fff35ecf0912066ea705895b3028fad141197d7ea2b56f1eef2a2516455e6f35d318f6fa39e2bb40492ac4ae603260790f7ebc7ea69feb4ca4c0a" + ) + + def test_nem_signtx_mosaic_creation(self): + self.setup_mnemonic_nopin_nopassphrase() + + test_suite = { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "levy": {}, + "properties": {}, + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + } + + # not using client.nem_sign_tx() because of swiping + tx = self._nem_sign(2, test_suite) + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f7404c100000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000030160000000d000000696e697469616c537570706c7901000000301a0000000d000000737570706c794d757461626c650500000066616c7365190000000c0000007472616e7366657261626c650500000066616c7365000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "537adf4fd9bd5b46e204b2db0a435257a951ed26008305e0aa9e1201dafa4c306d7601a8dbacabf36b5137724386124958d53202015ab31fb3d0849dfed2df0e" + ) + + def test_nem_signtx_mosaic_creation_properties(self): + self.setup_mnemonic_nopin_nopassphrase() + + test_suite = { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "levy": {}, + "properties": [ + {"name": "divisibility", "value": "4"}, + {"name": "initialSupply", "value": "200"}, + {"name": "supplyMutable", "value": "false"}, + {"name": "transferable", "value": "true"}, + ], + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + } + + # not using client.nem_sign_tx() because of swiping + tx = self._nem_sign(2, test_suite) + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f7404c200000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000034180000000d000000696e697469616c537570706c79030000003230301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c650400000074727565000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "f17c859710060f2ea9a0ab740ef427431cf36bdc7d263570ca282bd66032e9f5737a921be9839429732e663be2bb74ccc16f34f5157ff2ef00a65796b54e800e" + ) + + def test_nem_signtx_mosaic_creation_levy(self): + self.setup_mnemonic_nopin_nopassphrase() + + test_suite = { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_MOSAIC_CREATION, + "deadline": 74735615, + "message": {}, + "mosaicDefinition": { + "id": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "properties": [ + {"name": "divisibility", "value": "4"}, + {"name": "initialSupply", "value": "200"}, + {"name": "supplyMutable", "value": "false"}, + {"name": "transferable", "value": "true"}, + ], + "levy": { + "type": 1, + "fee": 2, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "mosaicId": {"namespaceId": "hellom", "name": "Hello mosaic"}, + }, + "description": "lorem", + }, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + } + + tx = self._nem_sign(6, test_suite) + assert ( + tx.data.hex() + == "01400000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74041801000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f73616963050000006c6f72656d04000000150000000c00000064697669736962696c6974790100000034180000000d000000696e697469616c537570706c79030000003230301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c65040000007472756556000000010000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a1a0000000600000068656c6c6f6d0c00000048656c6c6f206d6f7361696302000000000000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000" + ) + assert ( + tx.signature.hex() + == "b87aac1ddf146d35e6a7f3451f57e2fe504ac559031e010a51261257c37bd50fcfa7b2939dd7a3203b54c4807d458475182f5d3dc135ec0d1d4a9cd42159fd0a" + ) + + def _nem_sign(self, num_of_swipes, test_suite): + n = parse_path("m/44'/1'/0'/0'/0'") + + def input_flow(): + # Confirm Action + btn_code = yield + assert btn_code == B.ConfirmOutput + self.client.debug.press_yes() + + # Swipe and confirm + time.sleep(1) + for i in range(num_of_swipes): + self.client.debug.swipe_down() + time.sleep(1) + self.client.debug.press_yes() + + # Confirm Action + btn_code = yield + assert btn_code == B.ConfirmOutput + self.client.debug.press_yes() + + # Sign Tx + btn_code = yield + assert btn_code == B.SignTx + self.client.debug.press_yes() + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=B.ConfirmOutput), + proto.ButtonRequest(code=B.ConfirmOutput), + proto.ButtonRequest(code=B.SignTx), + proto.NEMSignedTx(), + ] + ) + self.client.set_input_flow(input_flow) + return nem.sign_tx(self.client, n, test_suite) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_signtx_multisig.py b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_multisig.py new file mode 100644 index 000000000..3f21d35b4 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_multisig.py @@ -0,0 +1,202 @@ +# 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 . + +import pytest + +from trezorlib import nem +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +# assertion data from T1 +@pytest.mark.nem +class TestMsgNEMSignTxMultisig(TrezorTest): + def test_nem_signtx_aggregate_modification(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_AGGREGATE_MODIFICATION, + "deadline": 74735615, + "message": {}, + "modifications": [ + { + "modificationType": 1, # Add + "cosignatoryAccount": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + } + ], + "minCosignatories": {"relativeChange": 3}, + "version": (0x98 << 24), + }, + ) + assert ( + tx.data.hex() + == "01100000020000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f740401000000280000000100000020000000c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f878440400000003000000" + ) + assert ( + tx.signature.hex() + == "1200e552d8732ce3eae96719731194abfc5a09d98f61bb35684f4eeaeff15b1bdf326ee7b1bbbe89d3f68c8e07ad3daf72e4c7f031094ad2236b97918ad98601" + ) + + def test_nem_signtx_multisig(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 1, + "fee": 10000, + "type": nem.TYPE_MULTISIG, + "deadline": 74735615, + "otherTrans": { # simple transaction transfer + "timeStamp": 2, + "amount": 2000000, + "fee": 15000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 67890, + "message": { + "payload": b"test_nem_transaction_transfer".hex(), + "type": 1, + }, + "version": (0x98 << 24), + "signer": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "04100000010000980100000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620841027000000000000ff5f74049900000001010000010000980200000020000000c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844983a000000000000320901002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a80841e000000000025000000010000001d000000746573745f6e656d5f7472616e73616374696f6e5f7472616e73666572" + ) + assert ( + tx.signature.hex() + == "0cab2fddf2f02b5d7201675b9a71869292fe25ed33a366c7d2cbea7676fed491faaa03310079b7e17884b6ba2e3ea21c4f728d1cca8f190b8288207f6514820a" + ) + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 150, + "type": nem.TYPE_MULTISIG, + "deadline": 789, + "otherTrans": { + "timeStamp": 123456, + "fee": 2000, + "type": nem.TYPE_PROVISION_NAMESPACE, + "deadline": 100, + "message": {}, + "newPart": "ABCDE", + "rentalFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "rentalFee": 1500, + "parent": None, + "version": (0x98 << 24), + "signer": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "04100000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b40620849600000000000000150300007d000000012000000100009840e2010020000000c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844d007000000000000640000002800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000050000004142434445ffffffff" + ) + assert ( + tx.signature.hex() + == "c915ca3332380925f4050301cdc62269cf29437ac5955321b18da34e570c7fdbb1aec2940a2a553a2a5c90950a4db3c8d3ef899c1a108582e0657f66fbbb0b04" + ) + + def test_nem_signtx_multisig_signer(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 333, + "fee": 200, + "type": nem.TYPE_MULTISIG_SIGNATURE, + "deadline": 444, + "otherTrans": { # simple transaction transfer + "timeStamp": 555, + "amount": 2000000, + "fee": 2000000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 666, + "message": { + "payload": b"test_nem_transaction_transfer".hex(), + "type": 1, + }, + "version": (0x98 << 24), + "signer": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "02100000010000984d01000020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b4062084c800000000000000bc010000240000002000000087923cd4805f3babe6b5af9cbb2b08be4458e39531618aed73c911f160c8e38528000000544444324354364c514c49595135364b49584933454e544d36454b3344343450354b5a50464d4b32" + ) + assert ( + tx.signature.hex() + == "286358a16ae545bff798feab93a713440c7c2f236d52ac0e995669d17a1915b0903667c97fa04418eccb42333cba95b19bccc8ac1faa8224dcfaeb41890ae807" + ) + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 900000, + "fee": 200000, + "type": nem.TYPE_MULTISIG_SIGNATURE, + "deadline": 100, + "otherTrans": { # simple transaction transfer + "timeStamp": 101111, + "fee": 1000, + "type": nem.TYPE_MOSAIC_SUPPLY_CHANGE, + "deadline": 13123, + "message": {}, + "mosaicId": {"namespaceId": "hellom", "name": "Hello mosaic"}, + "supplyType": 1, + "delta": 1, + "version": (0x98 << 24), + "creationFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "creationFee": 1500, + "signer": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "0210000001000098a0bb0d0020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b4062084400d030000000000640000002400000020000000c51395626a89a71c1ed785fb5974307a049b3b9e2165d56ed0302fe6b4f02a0128000000544444324354364c514c49595135364b49584933454e544d36454b3344343450354b5a50464d4b32" + ) + assert ( + tx.signature.hex() + == "32b1fdf788c4a90c01eedf5972b7709745831d620c13e1e97b0de6481837e162ee551573f2409822754ae940731909ec4b79cf836487e898df476adb10467506" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_signtx_others.py b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_others.py new file mode 100644 index 000000000..7d53b8d4c --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_others.py @@ -0,0 +1,86 @@ +# 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 . + +import pytest + +from trezorlib import nem +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +# assertion data from T1 +@pytest.mark.nem +class TestMsgNEMSignTxOther(TrezorTest): + def test_nem_signtx_importance_transfer(self): + self.setup_mnemonic_nopin_nopassphrase() + + with self.client: + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 12349215, + "fee": 9900, + "type": nem.TYPE_IMPORTANCE_TRANSFER, + "deadline": 99, + "message": {}, + "importanceTransfer": { + "mode": 1, # activate + "publicKey": "c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844", + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "01080000010000981f6fbc0020000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b4062084ac26000000000000630000000100000020000000c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844" + ) + assert ( + tx.signature.hex() + == "b6d9434ec5df80e65e6e45d7f0f3c579b4adfe8567c42d981b06e8ac368b1aad2b24eebecd5efd41f4497051fca8ea8a5e77636a79afc46ee1a8e0fe9e3ba90b" + ) + + def test_nem_signtx_provision_namespace(self): + + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "fee": 2000000, + "type": nem.TYPE_PROVISION_NAMESPACE, + "deadline": 74735615, + "message": {}, + "newPart": "ABCDE", + "rentalFeeSink": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "rentalFee": 1500, + "parent": None, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "01200000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74042800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324adc05000000000000050000004142434445ffffffff" + ) + assert ( + tx.signature.hex() + == "f047ae7987cd3a60c0d5ad123aba211185cb6266a7469dfb0491a0df6b5cd9c92b2e2b9f396cc2a3146ee185ba02df4f9e7fb238fe479917b3d274d97336640d" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_nem_signtx_transfers.py b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_transfers.py new file mode 100644 index 000000000..60391940f --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_nem_signtx_transfers.py @@ -0,0 +1,304 @@ +# 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 . + +import pytest + +from trezorlib import messages as proto, nem +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +# assertion data from T1 +@pytest.mark.nem +class TestMsgNEMSignTx(TrezorTest): + def test_nem_signtx_simple(self): + # tx hash: 209368053ac61969b6838ceb7e31badeb622ed6aa42d6c58365c42ad1a11e19d + self.setup_mnemonic_nopin_nopassphrase() + with self.client: + self.client.set_expected_responses( + [ + # Confirm transfer and network fee + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + # Unencrypted message + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + # Confirm recipient + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.NEMSignedTx(), + ] + ) + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "amount": 2000000, + "fee": 2000000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 74735615, + "message": { + "payload": b"test_nem_transaction_transfer".hex(), + "type": 1, + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data.hex() + == "01010000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74042800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a80841e000000000025000000010000001d000000746573745f6e656d5f7472616e73616374696f6e5f7472616e73666572" + ) + assert ( + tx.signature.hex() + == "9cda2045324d05c791a4fc312ecceb62954e7740482f8df8928560d63cf273dea595023640179f112de755c79717757ef76962175378d6d87360ddb3f3e5f70f" + ) + + def test_nem_signtx_encrypted_payload(self): + self.setup_mnemonic_nopin_nopassphrase() + + with self.client: + self.client.set_expected_responses( + [ + # Confirm transfer and network fee + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + # Ask for encryption + proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput), + # Confirm recipient + proto.ButtonRequest(code=proto.ButtonRequestType.SignTx), + proto.NEMSignedTx(), + ] + ) + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 74649215, + "amount": 2000000, + "fee": 2000000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 74735615, + "message": { + # plain text is 32B long => cipher text is 48B + # as per PKCS#7 another block containing padding is added + "payload": b"this message should be encrypted".hex(), + "publicKey": "5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541", + "type": 2, + }, + "version": (0x98 << 24), + }, + ) + + assert ( + tx.data[:124].hex() + == "01010000010000987f0e730420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208480841e0000000000ff5f74042800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a80841e0000000000680000000200000060000000" + ) + # after 124th byte comes iv (16B) salt (32B) and encrypted payload (48B) + assert len(tx.data[124:]) == 16 + 32 + 48 + # because IV and salt are random (therefore the encrypted payload as well) those data can't be asserted + assert len(tx.signature) == 64 + + def test_nem_signtx_xem_as_mosaic(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 76809215, + "amount": 5000000, + "fee": 1000000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 76895615, + "version": (0x98 << 24), + "message": {}, + "mosaics": [ + { + "mosaicId": {"namespaceId": "nem", "name": "xem"}, + "quantity": 9000000, + } + ], + }, + ) + + # trezor should display 45 XEM (multiplied by amount) + assert ( + tx.data.hex() + == "0101000002000098ff03940420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208440420f00000000007f5595042800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a404b4c000000000000000000010000001a0000000e000000030000006e656d0300000078656d4054890000000000" + ) + assert ( + tx.signature.hex() + == "7b25a84b65adb489ea55739f1ca2d83a0ae069c3c58d0ea075fc30bfe8f649519199ad2324ca229c6c3214191469f95326e99712124592cae7cd3a092c93ac0c" + ) + + def test_nem_signtx_unknown_mosaic(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 76809215, + "amount": 2000000, + "fee": 1000000, + "recipient": "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 76895615, + "version": (0x98 << 24), + "message": {}, + "mosaics": [ + { + "mosaicId": {"namespaceId": "xxx", "name": "aa"}, + "quantity": 3500000, + } + ], + }, + ) + + # trezor should display warning about unknown mosaic and then dialog for 7000000 raw units of xxx.aa and 0 XEM + assert ( + tx.data.hex() + == "0101000002000098ff03940420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208440420f00000000007f5595042800000054414c49434532474d4133344358484437584c4a513533364e4d35554e4b5148544f524e4e54324a80841e00000000000000000001000000190000000d00000003000000787878020000006161e067350000000000" + ) + assert ( + tx.signature.hex() + == "2f0280420eceb41ef9e5d94fa44ddda9cdc70b8f423ae18af577f6d85df64bb4aaf40cf24fc6eef47c63b0963611f8682348cecdc49a9b64eafcbe7afcb49102" + ) + + def test_nem_signtx_known_mosaic(self): + + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 76809215, + "amount": 3000000, + "fee": 1000000, + "recipient": "NDMYSLXI4L3FYUQWO4MJOVL6BSTJJXKDSZRMT4LT", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 76895615, + "version": (0x68 << 24), + "message": {}, + "mosaics": [ + { + "mosaicId": {"namespaceId": "dim", "name": "token"}, + "quantity": 111000, + } + ], + }, + ) + + # trezor should display 0 XEM and 0.333 DIMTOK + assert ( + tx.data.hex() + == "0101000002000068ff03940420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208440420f00000000007f559504280000004e444d59534c5849344c3346595551574f344d4a4f564c364253544a4a584b44535a524d54344c54c0c62d000000000000000000010000001c000000100000000300000064696d05000000746f6b656e98b1010000000000" + ) + assert ( + tx.signature.hex() + == "e7f14ef8c39727bfd257e109cd5acac31542f2e41f2e5deb258fc1db602b690eb1cabca41a627fe2adc51f3193db85c76b41c80bb60161eb8738ebf20b507104" + ) + + def test_nem_signtx_known_mosaic_with_levy(self): + + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 76809215, + "amount": 2000000, + "fee": 1000000, + "recipient": "NDMYSLXI4L3FYUQWO4MJOVL6BSTJJXKDSZRMT4LT", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 76895615, + "version": (0x68 << 24), + "message": {}, + "mosaics": [ + { + "mosaicId": {"namespaceId": "dim", "name": "coin"}, + "quantity": 222000, + } + ], + }, + ) + + # trezor should display 0 XEM and 0.444 DIM and levy of 0.000444 DIM + assert ( + tx.data.hex() + == "0101000002000068ff03940420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208440420f00000000007f559504280000004e444d59534c5849344c3346595551574f344d4a4f564c364253544a4a584b44535a524d54344c5480841e000000000000000000010000001b0000000f0000000300000064696d04000000636f696e3063030000000000" + ) + assert ( + tx.signature.hex() + == "d3222dd7b83d66bda0539827ac6f909d06e40350b5e5e893d6fa762f954e9bf7da61022ef04950e7b6dfa88a2278f2f8a1b21df2bc3af22b388cb3a90bf76f07" + ) + + def test_nem_signtx_multiple_mosaics(self): + self.setup_mnemonic_nopin_nopassphrase() + + tx = nem.sign_tx( + self.client, + parse_path("m/44'/1'/0'/0'/0'"), + { + "timeStamp": 76809215, + "amount": 2000000, + "fee": 1000000, + "recipient": "NDMYSLXI4L3FYUQWO4MJOVL6BSTJJXKDSZRMT4LT", + "type": nem.TYPE_TRANSACTION_TRANSFER, + "deadline": 76895615, + "version": (0x68 << 24), + "message": {}, + "mosaics": [ + { + "mosaicId": {"namespaceId": "nem", "name": "xem"}, + "quantity": 3000000, + }, + { + "mosaicId": {"namespaceId": "abc", "name": "mosaic"}, + "quantity": 200, + }, + { + "mosaicId": {"namespaceId": "nem", "name": "xem"}, + "quantity": 30000, + }, + { + "mosaicId": {"namespaceId": "abc", "name": "mosaic"}, + "quantity": 2000000, + }, + { + "mosaicId": {"namespaceId": "breeze", "name": "breeze-token"}, + "quantity": 111000, + }, + ], + }, + ) + + # trezor should display warning, 6.06 XEM, 4000400 raw units of abc.mosaic (mosaics are merged) + # and 222000 BREEZE + assert ( + tx.data.hex() + == "0101000002000068ff03940420000000edfd32f6e760648c032f9acb4b30d514265f6a5b5f8a7154f2618922b406208440420f00000000007f559504280000004e444d59534c5849344c3346595551574f344d4a4f564c364253544a4a584b44535a524d54344c5480841e000000000000000000030000001d0000001100000003000000616263060000006d6f7361696348851e0000000000260000001a00000006000000627265657a650c000000627265657a652d746f6b656e98b10100000000001a0000000e000000030000006e656d0300000078656df03b2e0000000000" + ) + assert ( + tx.signature.hex() + == "b2b9319fca87a05bee17108edd9a8f78aeffef74bf6b4badc6da5d46e8ff4fe82e24bf69d8e6c4097d072adf39d0c753e7580f8afb21e3288ebfb7c4d84e470d" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ontology_getaddress.py b/python/trezorlib/tests/device_tests/test_msg_ontology_getaddress.py new file mode 100644 index 000000000..9e1cc1be5 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ontology_getaddress.py @@ -0,0 +1,43 @@ +# 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 . + +import pytest + +from trezorlib import ontology +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.xfail +@pytest.mark.ontology +@pytest.mark.skip_t1 +class TestMsgOntologyGetaddress(TrezorTest): + def test_ontology_get_ont_address(self): + self.setup_mnemonic_nopin_nopassphrase() + + assert ( + ontology.get_address(self.client, parse_path("m/44'/1024'/0'/0/0")) + == "ANzeepWmi9hoLBA3UiwVhUm7Eku196VUHk" + ) + + def test_ontology_get_neo_address(self): + self.setup_mnemonic_nopin_nopassphrase() + + assert ( + ontology.get_address(self.client, parse_path("m/44'/888'/0'/0/0")) + == "AZEMburLePcdfqBFnVfdbsXKiBSnmtgFZr" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_add_attributes.py b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_add_attributes.py new file mode 100644 index 000000000..20bdc0676 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_add_attributes.py @@ -0,0 +1,98 @@ +# 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 . + +import time + +import pytest + +from trezorlib import messages, ontology +from trezorlib.messages import ButtonRequestType as B +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.xfail +@pytest.mark.ontology +@pytest.mark.skip_t1 +class TestMsgOntologySignOntIdAddAttributes(TrezorTest): + def test_ontology_sign_ont_id_add_attributes(self): + self.setup_mnemonic_nopin_nopassphrase() + + transaction = messages.OntologyTransaction( + version=0x00, + nonce=0x7F7F1CEB, + type=0xD1, + gas_price=500, + gas_limit=30000, + payer="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + tx_attributes=[], + ) + + ont_id_add_attributes = messages.OntologyOntIdAddAttributes( + ont_id="did:ont:AGVn4NZNEQ7RawHTDxjaTjZ3R8h8q1aq9h", + public_key=bytes.fromhex( + "03a8269b0dad311d98195e76729bc57003348a315fd17b6bf4f90ba8b86735fa33" + ), + ont_id_attributes=[ + messages.OntologyOntIdAttribute( + key="firstName", type="json", value="John Sheppard" + ) + ], + ) + + # not using ontology.sign_add_attr() because of swiping + signature = self._ontology_sign( + 2, parse_path("m/44'/1024'/0'/0/0"), transaction, ont_id_add_attributes + ) + + assert ( + signature.payload.hex() + == "bd00c66b2a6469643a6f6e743a4147566e344e5a4e455137526177485444786a61546a5a33523868387131617139686a7cc8516a7cc80966697273744e616d656a7cc8046a736f6e6a7cc80d4a6f686e2053686570706172646a7cc82103a8269b0dad311d98195e76729bc57003348a315fd17b6bf4f90ba8b86735fa336a7cc86c0d616464417474726962757465731400000000000000000000000000000000000000030068164f6e746f6c6f67792e4e61746976652e496e766f6b65" + ) + assert ( + signature.signature.hex() + == "01c256dc16d88685fd6652d69b808059f7ed30edadb0ccfe51802702b94b65500922f9ea80e0fd7b77b5c51515e3bc43a495b3e98fb3adb82a0ab5dd47169fcf4e" + ) + + def _ontology_sign( + self, num_of_swipes, address_n, transaction, ont_id_add_attributes + ): + def input_flow(): + # Sign Tx + btn_code = yield + assert btn_code == B.SignTx + + # Swipe and confirm + time.sleep(1) + for _ in range(num_of_swipes): + self.client.debug.swipe_down() + time.sleep(1) + + # Confirm Action + self.client.debug.press_yes() + + with self.client: + self.client.set_input_flow(input_flow) + self.client.set_expected_responses( + [ + messages.ButtonRequest(code=B.SignTx), + messages.OntologySignedOntIdAddAttributes(), + ] + ) + return ontology.sign_add_attr( + self.client, address_n, transaction, ont_id_add_attributes + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_register.py b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_register.py new file mode 100644 index 000000000..7c775e8a3 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_ont_id_register.py @@ -0,0 +1,90 @@ +# 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 . + +import time + +import pytest + +from trezorlib import messages, ontology +from trezorlib.messages import ButtonRequestType as B +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.xfail +@pytest.mark.ontology +@pytest.mark.skip_t1 +class TestMsgOntologySignOntIdRegister(TrezorTest): + def test_ontology_sign_ont_id_register(self): + self.setup_mnemonic_nopin_nopassphrase() + + transaction = messages.OntologyTransaction( + version=0x00, + nonce=0x7F7F1CEB, + type=0xD1, + gas_price=500, + gas_limit=30000, + payer="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + tx_attributes=[], + ) + + ont_id_register = messages.OntologyOntIdRegister( + ont_id="did:ont:AGVn4NZNEQ7RawHTDxjaTjZ3R8h8q1aq9h", + public_key=bytes.fromhex( + "03a8269b0dad311d98195e76729bc57003348a315fd17b6bf4f90ba8b86735fa33" + ), + ) + + # not using ontology.sign_register() because of swiping + signature = self._ontology_sign( + 1, parse_path("m/44'/1024'/0'/0/0"), transaction, ont_id_register + ) + assert ( + signature.payload.hex() + == "9800c66b2a6469643a6f6e743a4147566e344e5a4e455137526177485444786a61546a5a33523868387131617139686a7cc82103a8269b0dad311d98195e76729bc57003348a315fd17b6bf4f90ba8b86735fa336a7cc86c127265674944576974685075626c69634b65791400000000000000000000000000000000000000030068164f6e746f6c6f67792e4e61746976652e496e766f6b65" + ) + assert ( + signature.signature.hex() + == "015d6abe231352d1ab32f0b0de0222cfb9a7a13f467a2bf8a369b61aa1f933dc3a6a2ba7831c8a15984fe0958d24cbca05d8e0736510c1734d773145ce3eac9e9b" + ) + + def _ontology_sign(self, num_of_swipes, address_n, transaction, ont_id_register): + def input_flow(): + # Sign Tx + btn_code = yield + assert btn_code == B.SignTx + + # Swipe and confirm + time.sleep(1) + for _ in range(num_of_swipes): + self.client.debug.swipe_down() + time.sleep(1) + + # Confirm Action + self.client.debug.press_yes() + + with self.client: + self.client.set_expected_responses( + [ + messages.ButtonRequest(code=B.SignTx), + messages.OntologySignedOntIdRegister(), + ] + ) + self.client.set_input_flow(input_flow) + return ontology.sign_register( + self.client, address_n, transaction, ont_id_register + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ontology_sign_tx.py b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_tx.py new file mode 100644 index 000000000..be0f9fe20 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_tx.py @@ -0,0 +1,87 @@ +# 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 . + +import pytest + +from trezorlib import messages, ontology +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.xfail +@pytest.mark.ontology +@pytest.mark.skip_t1 +class TestMsgOntologySigntx(TrezorTest): + def test_ontology_sign_transfer_ont(self): + self.setup_mnemonic_nopin_nopassphrase() + + transaction = messages.OntologyTransaction( + version=0x00, + nonce=0x7F7F1CEB, + type=0xD1, + gas_price=500, + gas_limit=30000, + payer="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + tx_attributes=[], + ) + + transfer = messages.OntologyTransfer( + asset=1, + amount=100, + from_address="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + to_address="AcyLq3tokVpkMBMLALVMWRdVJ83TTgBUwU", + ) + + signature = ontology.sign_transfer( + self.client, parse_path("m/44'/1024'/0'/0/0"), transaction, transfer + ) + assert ( + signature.payload.hex() + == "7900c66b140b045b101bc9fabaf181e251a38e76b73962111b6a7cc814e885e849e7f545ea84e8c555b86c70e4f751c4ec6a7cc80864000000000000006a7cc86c51c1087472616e736665721400000000000000000000000000000000000000010068164f6e746f6c6f67792e4e61746976652e496e766f6b65" + ) + assert ( + signature.signature.hex() + == "0102f9b0c43b2ed35aa89b0927a60e692cb8a74280c2da819a909150c8b3fd2b0b401806c97797fcc4b93d34f210ad01740cfd13b720a389a80f384c1f94fb749e" + ) + + def test_ontology_sign_transfer_ong(self): + self.setup_mnemonic_nopin_nopassphrase() + + transaction = messages.OntologyTransaction( + version=0x00, + nonce=0x7F7F1CEB, + type=0xD1, + gas_price=500, + gas_limit=30000, + payer="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + tx_attributes=[], + ) + + transfer = messages.OntologyTransfer( + asset=2, + amount=12000000, + from_address="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + to_address="AcyLq3tokVpkMBMLALVMWRdVJ83TTgBUwU", + ) + + signature = ontology.sign_transfer( + self.client, parse_path("m/44'/1024'/0'/0/0"), transaction, transfer + ) + assert ( + signature.signature.hex() + == "01ad88061a6cf5f4960cf9d311adb6dec4925d368b0fa9b7f56269f2a4078bea2367469af50c70260142d2ce3cc2d1e7fd0b2923df659c994412ff18f138438e9d" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ontology_sign_withdraw_ong.py b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_withdraw_ong.py new file mode 100644 index 000000000..6bcfa442d --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ontology_sign_withdraw_ong.py @@ -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 . + +import pytest + +from trezorlib import messages, ontology +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.xfail +@pytest.mark.ontology +@pytest.mark.skip_t1 +class TestMsgOntologySignWithdraw(TrezorTest): + def test_ontology_sign_withdraw_ong(self): + self.setup_mnemonic_nopin_nopassphrase() + + transaction = messages.OntologyTransaction( + version=0x00, + nonce=0x7F7F1CEB, + type=0xD1, + gas_price=500, + gas_limit=30000, + payer="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + tx_attributes=[], + ) + + withdraw_ong = messages.OntologyWithdrawOng( + amount=12000000, + from_address="AGn8JFPGM5S4jkWhTC89Xtz1Y76sPz29Rc", + to_address="AcyLq3tokVpkMBMLALVMWRdVJ83TTgBUwU", + ) + + signature = ontology.sign_withdrawal( + self.client, parse_path("m/44'/1024'/0'/0/0"), transaction, withdraw_ong + ) + assert ( + signature.payload.hex() + == "9300c66b140b045b101bc9fabaf181e251a38e76b73962111b6a7cc81400000000000000000000000000000000000000016a7cc814e885e849e7f545ea84e8c555b86c70e4f751c4ec6a7cc808001bb700000000006a7cc86c0c7472616e7366657246726f6d1400000000000000000000000000000000000000020068164f6e746f6c6f67792e4e61746976652e496e766f6b65" + ) + assert ( + signature.signature.hex() + == "01a44355ac4549a021ecc571eb85ffb6ae4ff50cffc416ec55df40cad538fa55c64386167df2fb6b3fa9e698ebe265088839667b88da7e599ce7df679b0d5dfe60" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_ping.py b/python/trezorlib/tests/device_tests/test_msg_ping.py new file mode 100644 index 000000000..6a16197e4 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ping.py @@ -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 . + +import pytest + +from trezorlib import messages as proto + +from .common import TrezorTest + + +@pytest.mark.skip_t2 +class TestMsgPing(TrezorTest): + def test_ping(self): + self.setup_mnemonic_pin_passphrase() + + with self.client: + self.client.set_expected_responses([proto.Success()]) + res = self.client.ping("random data") + assert res == "random data" + + with self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), + proto.Success(), + ] + ) + res = self.client.ping("random data", button_protection=True) + assert res == "random data" + + with self.client: + self.client.set_expected_responses( + [proto.PinMatrixRequest(), proto.Success()] + ) + res = self.client.ping("random data", pin_protection=True) + assert res == "random data" + + with self.client: + self.client.set_expected_responses( + [proto.PassphraseRequest(), proto.Success()] + ) + res = self.client.ping("random data", passphrase_protection=True) + assert res == "random data" + + def test_ping_caching(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" diff --git a/python/trezorlib/tests/device_tests/test_msg_recoverydevice.py b/python/trezorlib/tests/device_tests/test_msg_recoverydevice.py new file mode 100644 index 000000000..342c238d1 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_recoverydevice.py @@ -0,0 +1,211 @@ +# 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 . + +import pytest + +from trezorlib import device, messages as proto + +from .common import TrezorTest + + +@pytest.mark.skip_t2 +class TestMsgRecoverydevice(TrezorTest): + def test_pin_passphrase(self): + mnemonic = self.mnemonic12.split(" ") + ret = self.client.call_raw( + proto.RecoveryDevice( + word_count=12, + passphrase_protection=True, + pin_protection=True, + label="label", + language="english", + enforce_wordlist=True, + ) + ) + + # click through confirmation + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for first time + pin_encoded = self.client.debug.encode_pin(self.pin6) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for second time + pin_encoded = self.client.debug.encode_pin(self.pin6) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + + fakes = 0 + for _ in range(int(12 * 2)): + assert isinstance(ret, proto.WordRequest) + (word, pos) = self.client.debug.read_recovery_word() + + if pos != 0: + ret = self.client.call_raw(proto.WordAck(word=mnemonic[pos - 1])) + mnemonic[pos - 1] = None + else: + ret = self.client.call_raw(proto.WordAck(word=word)) + fakes += 1 + + print(mnemonic) + + # Workflow succesfully ended + assert isinstance(ret, proto.Success) + + # 12 expected fake words and all words of mnemonic are used + assert fakes == 12 + assert mnemonic == [None] * 12 + + # Mnemonic is the same + self.client.init_device() + assert self.client.debug.read_mnemonic_secret() == self.mnemonic12.encode() + + assert self.client.features.pin_protection is True + assert self.client.features.passphrase_protection is True + + # Do passphrase-protected action, PassphraseRequest should be raised + resp = self.client.call_raw(proto.Ping(passphrase_protection=True)) + assert isinstance(resp, proto.PassphraseRequest) + self.client.call_raw(proto.Cancel()) + + def test_nopin_nopassphrase(self): + mnemonic = self.mnemonic12.split(" ") + ret = self.client.call_raw( + proto.RecoveryDevice( + word_count=12, + passphrase_protection=False, + pin_protection=False, + label="label", + language="english", + enforce_wordlist=True, + ) + ) + + # click through confirmation + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + fakes = 0 + for _ in range(int(12 * 2)): + assert isinstance(ret, proto.WordRequest) + (word, pos) = self.client.debug.read_recovery_word() + + if pos != 0: + ret = self.client.call_raw(proto.WordAck(word=mnemonic[pos - 1])) + mnemonic[pos - 1] = None + else: + ret = self.client.call_raw(proto.WordAck(word=word)) + fakes += 1 + + print(mnemonic) + + # Workflow succesfully ended + assert isinstance(ret, proto.Success) + + # 12 expected fake words and all words of mnemonic are used + assert fakes == 12 + assert mnemonic == [None] * 12 + + # Mnemonic is the same + self.client.init_device() + assert self.client.debug.read_mnemonic_secret() == self.mnemonic12.encode() + + assert self.client.features.pin_protection is False + assert self.client.features.passphrase_protection is False + + # Do passphrase-protected action, PassphraseRequest should NOT be raised + resp = self.client.call_raw(proto.Ping(passphrase_protection=True)) + assert isinstance(resp, proto.Success) + + # Do PIN-protected action, PinRequest should NOT be raised + resp = self.client.call_raw(proto.Ping(pin_protection=True)) + assert isinstance(resp, proto.Success) + + def test_word_fail(self): + ret = self.client.call_raw( + proto.RecoveryDevice( + word_count=12, + passphrase_protection=False, + pin_protection=False, + label="label", + language="english", + enforce_wordlist=True, + ) + ) + + # click through confirmation + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.WordRequest) + for _ in range(int(12 * 2)): + (word, pos) = self.client.debug.read_recovery_word() + if pos != 0: + ret = self.client.call_raw(proto.WordAck(word="kwyjibo")) + assert isinstance(ret, proto.Failure) + break + else: + self.client.call_raw(proto.WordAck(word=word)) + + def test_pin_fail(self): + ret = self.client.call_raw( + proto.RecoveryDevice( + word_count=12, + passphrase_protection=True, + pin_protection=True, + label="label", + language="english", + enforce_wordlist=True, + ) + ) + + # click through confirmation + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for first time + pin_encoded = self.client.debug.encode_pin(self.pin4) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for second time, but different one + pin_encoded = self.client.debug.encode_pin(self.pin6) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + + # Failure should be raised + assert isinstance(ret, proto.Failure) + + def test_already_initialized(self): + self.setup_mnemonic_nopin_nopassphrase() + with pytest.raises(RuntimeError): + device.recover( + self.client, + 12, + False, + False, + "label", + "english", + self.client.mnemonic_callback, + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_recoverydevice_dryrun.py b/python/trezorlib/tests/device_tests/test_msg_recoverydevice_dryrun.py new file mode 100644 index 000000000..d9284e61c --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_recoverydevice_dryrun.py @@ -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 . + +import pytest + +from trezorlib import messages as proto + +from .common import TrezorTest + + +@pytest.mark.skip_t2 +class TestMsgRecoverydeviceDryrun(TrezorTest): + def recovery_loop(self, mnemonic, result): + ret = self.client.call_raw( + proto.RecoveryDevice( + word_count=12, + passphrase_protection=False, + pin_protection=False, + label="label", + language="english", + enforce_wordlist=True, + dry_run=True, + ) + ) + + fakes = 0 + for _ in range(int(12 * 2)): + assert isinstance(ret, proto.WordRequest) + (word, pos) = self.client.debug.read_recovery_word() + + if pos != 0: + ret = self.client.call_raw(proto.WordAck(word=mnemonic[pos - 1])) + mnemonic[pos - 1] = None + else: + ret = self.client.call_raw(proto.WordAck(word=word)) + fakes += 1 + + print(mnemonic) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + + ret = self.client.call_raw(proto.ButtonAck()) + assert isinstance(ret, result) + + def test_correct_notsame(self): + self.setup_mnemonic_nopin_nopassphrase() + mnemonic = ["all"] * 12 + self.recovery_loop(mnemonic, proto.Failure) + + def test_correct_same(self): + self.setup_mnemonic_nopin_nopassphrase() + mnemonic = self.mnemonic12.split(" ") + self.recovery_loop(mnemonic, proto.Success) + + def test_incorrect(self): + self.setup_mnemonic_nopin_nopassphrase() + mnemonic = ["stick"] * 12 + self.recovery_loop(mnemonic, proto.Failure) diff --git a/python/trezorlib/tests/device_tests/test_msg_recoverydevice_t2.py b/python/trezorlib/tests/device_tests/test_msg_recoverydevice_t2.py new file mode 100644 index 000000000..8748e6a00 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_recoverydevice_t2.py @@ -0,0 +1,131 @@ +# 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 . + +import time + +import pytest + +from trezorlib import device, messages as proto + +from .common import TrezorTest + + +@pytest.mark.skip_t1 +class TestMsgRecoverydeviceT2(TrezorTest): + def test_pin_passphrase(self): + mnemonic = self.mnemonic12.split(" ") + ret = self.client.call_raw( + proto.RecoveryDevice( + passphrase_protection=True, + pin_protection=True, + label="label", + enforce_wordlist=True, + ) + ) + + # Confirm Recovery + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter word count + assert ret == proto.ButtonRequest( + code=proto.ButtonRequestType.MnemonicWordCount + ) + self.client.debug.input(str(len(mnemonic))) + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter mnemonic words + assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.MnemonicInput) + self.client.transport.write(proto.ButtonAck()) + for word in mnemonic: + time.sleep(1) + self.client.debug.input(word) + ret = self.client.transport.read() + + # Enter PIN for first time + assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.Other) + self.client.debug.input("654") + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter PIN for second time + assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.Other) + self.client.debug.input("654") + ret = self.client.call_raw(proto.ButtonAck()) + + # Workflow succesfully ended + assert ret == proto.Success(message="Device recovered") + + # Mnemonic is the same + self.client.init_device() + assert self.client.debug.read_mnemonic_secret() == self.mnemonic12.encode() + + assert self.client.features.pin_protection is True + assert self.client.features.passphrase_protection is True + + def test_nopin_nopassphrase(self): + mnemonic = self.mnemonic12.split(" ") + ret = self.client.call_raw( + proto.RecoveryDevice( + passphrase_protection=False, + pin_protection=False, + label="label", + enforce_wordlist=True, + ) + ) + + # Confirm Recovery + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter word count + assert ret == proto.ButtonRequest( + code=proto.ButtonRequestType.MnemonicWordCount + ) + self.client.debug.input(str(len(mnemonic))) + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter mnemonic words + assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.MnemonicInput) + self.client.transport.write(proto.ButtonAck()) + for word in mnemonic: + time.sleep(1) + self.client.debug.input(word) + ret = self.client.transport.read() + + # Workflow succesfully ended + assert ret == proto.Success(message="Device recovered") + + # Mnemonic is the same + self.client.init_device() + assert self.client.debug.read_mnemonic_secret() == self.mnemonic12.encode() + + assert self.client.features.pin_protection is False + assert self.client.features.passphrase_protection is False + + def test_already_initialized(self): + self.setup_mnemonic_nopin_nopassphrase() + with pytest.raises(RuntimeError): + device.recover( + self.client, + 12, + False, + False, + "label", + "english", + self.client.mnemonic_callback, + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_resetdevice.py b/python/trezorlib/tests/device_tests/test_msg_resetdevice.py new file mode 100644 index 000000000..8b57837ac --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_resetdevice.py @@ -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 . + +import pytest +from mnemonic import Mnemonic + +from trezorlib import device, messages as proto + +from .common import TrezorTest, generate_entropy + + +@pytest.mark.skip_t2 +class TestMsgResetDevice(TrezorTest): + def test_reset_device(self): + + # No PIN, no passphrase + external_entropy = b"zlutoucky kun upel divoke ody" * 2 + strength = 128 + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=False, + strength=strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Provide entropy + assert isinstance(ret, proto.EntropyRequest) + internal_entropy = self.client.debug.read_reset_entropy() + ret = self.client.call_raw(proto.EntropyAck(entropy=external_entropy)) + + # Generate mnemonic locally + entropy = generate_entropy(strength, internal_entropy, external_entropy) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + mnemonic = [] + for _ in range(strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + self.client.call_raw(proto.ButtonAck()) + + mnemonic = " ".join(mnemonic) + + # Compare that device generated proper mnemonic for given entropies + assert mnemonic == expected_mnemonic + + mnemonic = [] + for _ in range(strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + resp = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(resp, proto.Success) + + mnemonic = " ".join(mnemonic) + + # Compare that second pass printed out the same mnemonic once again + assert mnemonic == expected_mnemonic + + # Check if device is properly initialized + resp = self.client.call_raw(proto.Initialize()) + assert resp.initialized is True + assert resp.needs_backup is False + assert resp.pin_protection is False + assert resp.passphrase_protection is False + + # Do passphrase-protected action, PassphraseRequest should NOT be raised + resp = self.client.call_raw(proto.Ping(passphrase_protection=True)) + assert isinstance(resp, proto.Success) + + # Do PIN-protected action, PinRequest should NOT be raised + resp = self.client.call_raw(proto.Ping(pin_protection=True)) + assert isinstance(resp, proto.Success) + + def test_reset_device_pin(self): + external_entropy = b"zlutoucky kun upel divoke ody" * 2 + strength = 128 + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=True, + strength=strength, + passphrase_protection=True, + pin_protection=True, + language="english", + label="test", + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for first time + pin_encoded = self.client.debug.encode_pin("654") + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for second time + pin_encoded = self.client.debug.encode_pin("654") + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + + # Provide entropy + assert isinstance(ret, proto.EntropyRequest) + internal_entropy = self.client.debug.read_reset_entropy() + ret = self.client.call_raw(proto.EntropyAck(entropy=external_entropy)) + + # Generate mnemonic locally + entropy = generate_entropy(strength, internal_entropy, external_entropy) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + mnemonic = [] + for _ in range(strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + self.client.call_raw(proto.ButtonAck()) + + mnemonic = " ".join(mnemonic) + + # Compare that device generated proper mnemonic for given entropies + assert mnemonic == expected_mnemonic + + mnemonic = [] + for _ in range(strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + resp = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(resp, proto.Success) + + mnemonic = " ".join(mnemonic) + + # Compare that second pass printed out the same mnemonic once again + assert mnemonic == expected_mnemonic + + # Check if device is properly initialized + resp = self.client.call_raw(proto.Initialize()) + assert resp.initialized is True + assert resp.needs_backup is False + assert resp.pin_protection is True + assert resp.passphrase_protection is True + + # Do passphrase-protected action, PassphraseRequest should be raised + resp = self.client.call_raw(proto.Ping(passphrase_protection=True)) + assert isinstance(resp, proto.PassphraseRequest) + self.client.call_raw(proto.Cancel()) + + def test_failed_pin(self): + # external_entropy = b'zlutoucky kun upel divoke ody' * 2 + strength = 128 + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=True, + strength=strength, + passphrase_protection=True, + pin_protection=True, + language="english", + label="test", + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for first time + pin_encoded = self.client.debug.encode_pin(self.pin4) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + assert isinstance(ret, proto.PinMatrixRequest) + + # Enter PIN for second time + pin_encoded = self.client.debug.encode_pin(self.pin6) + ret = self.client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) + + assert isinstance(ret, proto.Failure) + + def test_already_initialized(self): + self.setup_mnemonic_nopin_nopassphrase() + with pytest.raises(Exception): + device.reset(self.client, False, 128, True, True, "label", "english") diff --git a/python/trezorlib/tests/device_tests/test_msg_resetdevice_nobackup.py b/python/trezorlib/tests/device_tests/test_msg_resetdevice_nobackup.py new file mode 100644 index 000000000..61b93c57d --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_resetdevice_nobackup.py @@ -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 . + +from trezorlib import messages as proto + +from .common import TrezorTest + + +class TestMsgResetDeviceNobackup(TrezorTest): + + external_entropy = b"zlutoucky kun upel divoke ody" * 2 + strength = 128 + + def test_reset_device_no_backup(self): + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=False, + strength=self.strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + no_backup=True, + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Provide entropy + assert isinstance(ret, proto.EntropyRequest) + ret = self.client.call_raw(proto.EntropyAck(entropy=self.external_entropy)) + assert isinstance(ret, proto.Success) + + # Check if device is properly initialized + ret = self.client.call_raw(proto.Initialize()) + assert ret.initialized is True + assert ret.needs_backup is False + assert ret.unfinished_backup is False + assert ret.no_backup is True + + # start backup - should fail + ret = self.client.call_raw(proto.BackupDevice()) + assert isinstance(ret, proto.Failure) + + def test_reset_device_no_backup_show_entropy_fail(self): + ret = self.client.call_raw( + proto.ResetDevice( + display_random=True, + strength=self.strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + no_backup=True, + ) + ) + assert isinstance(ret, proto.Failure) diff --git a/python/trezorlib/tests/device_tests/test_msg_resetdevice_skipbackup.py b/python/trezorlib/tests/device_tests/test_msg_resetdevice_skipbackup.py new file mode 100644 index 000000000..7da7858c0 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_resetdevice_skipbackup.py @@ -0,0 +1,171 @@ +# 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 . + +import pytest +from mnemonic import Mnemonic + +from trezorlib import messages as proto + +from .common import TrezorTest, generate_entropy + + +@pytest.mark.skip_t2 +class TestMsgResetDeviceSkipbackup(TrezorTest): + + external_entropy = b"zlutoucky kun upel divoke ody" * 2 + strength = 128 + + def test_reset_device_skip_backup(self): + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=False, + strength=self.strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + skip_backup=True, + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Provide entropy + assert isinstance(ret, proto.EntropyRequest) + internal_entropy = self.client.debug.read_reset_entropy() + ret = self.client.call_raw(proto.EntropyAck(entropy=self.external_entropy)) + assert isinstance(ret, proto.Success) + + # Check if device is properly initialized + ret = self.client.call_raw(proto.Initialize()) + assert ret.initialized is True + assert ret.needs_backup is True + assert ret.unfinished_backup is False + assert ret.no_backup is False + + # Generate mnemonic locally + entropy = generate_entropy( + self.strength, internal_entropy, self.external_entropy + ) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + # start Backup workflow + ret = self.client.call_raw(proto.BackupDevice()) + + mnemonic = [] + for _ in range(self.strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + self.client.call_raw(proto.ButtonAck()) + + mnemonic = " ".join(mnemonic) + + # Compare that device generated proper mnemonic for given entropies + assert mnemonic == expected_mnemonic + + mnemonic = [] + for _ in range(self.strength // 32 * 3): + assert isinstance(ret, proto.ButtonRequest) + mnemonic.append(self.client.debug.read_reset_word()) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.Success) + + mnemonic = " ".join(mnemonic) + + # Compare that second pass printed out the same mnemonic once again + assert mnemonic == expected_mnemonic + + # start backup again - should fail + ret = self.client.call_raw(proto.BackupDevice()) + assert isinstance(ret, proto.Failure) + + def test_reset_device_skip_backup_break(self): + + ret = self.client.call_raw( + proto.ResetDevice( + display_random=False, + strength=self.strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + skip_backup=True, + ) + ) + + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Provide entropy + assert isinstance(ret, proto.EntropyRequest) + ret = self.client.call_raw(proto.EntropyAck(entropy=self.external_entropy)) + assert isinstance(ret, proto.Success) + + # Check if device is properly initialized + ret = self.client.call_raw(proto.Initialize()) + assert ret.initialized is True + assert ret.needs_backup is True + assert ret.unfinished_backup is False + assert ret.no_backup is False + + # start Backup workflow + ret = self.client.call_raw(proto.BackupDevice()) + + # send Initialize -> break workflow + ret = self.client.call_raw(proto.Initialize()) + assert isinstance(ret, proto.Features) + assert ret.initialized is True + assert ret.needs_backup is False + assert ret.unfinished_backup is True + assert ret.no_backup is False + + # start backup again - should fail + ret = self.client.call_raw(proto.BackupDevice()) + assert isinstance(ret, proto.Failure) + + # read Features again + ret = self.client.call_raw(proto.Initialize()) + assert isinstance(ret, proto.Features) + assert ret.initialized is True + assert ret.needs_backup is False + assert ret.unfinished_backup is True + assert ret.no_backup is False + + def test_initialized_device_backup_fail(self): + self.setup_mnemonic_nopin_nopassphrase() + ret = self.client.call_raw(proto.BackupDevice()) + assert isinstance(ret, proto.Failure) + + def test_reset_device_skip_backup_show_entropy_fail(self): + ret = self.client.call_raw( + proto.ResetDevice( + display_random=True, + strength=self.strength, + passphrase_protection=False, + pin_protection=False, + language="english", + label="test", + skip_backup=True, + ) + ) + assert isinstance(ret, proto.Failure) diff --git a/python/trezorlib/tests/device_tests/test_msg_resetdevice_t2.py b/python/trezorlib/tests/device_tests/test_msg_resetdevice_t2.py new file mode 100644 index 000000000..e515bb01e --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_resetdevice_t2.py @@ -0,0 +1,236 @@ +# 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 . + +import time +from unittest import mock + +import pytest +from mnemonic import Mnemonic + +from trezorlib import device, messages as proto +from trezorlib.messages import ButtonRequestType as B + +from .common import TrezorTest, generate_entropy + +EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 + + +@pytest.mark.skip_t1 +class TestMsgResetDeviceT2(TrezorTest): + def test_reset_device(self): + words = [] + strength = 128 + + def input_flow(): + # Confirm Reset + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + # Backup your seed + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + # mnemonic phrases + btn_code = yield + assert btn_code == B.ResetDevice + # 12 words, 3 pages + for i in range(3): + time.sleep(1) + words.extend(self.client.debug.state().reset_word.split()) + if i < 2: + self.client.debug.swipe_down() + else: + # last page is confirmation + self.client.debug.press_yes() + + # check backup words + for _ in range(2): + time.sleep(1) + index = self.client.debug.state().reset_word_pos + self.client.debug.input(words[index]) + + # safety warning + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY) + with mock.patch("os.urandom", os_urandom), self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=B.ResetDevice), + proto.EntropyRequest(), + proto.ButtonRequest(code=B.ResetDevice), + proto.ButtonRequest(code=B.ResetDevice), + proto.ButtonRequest(code=B.ResetDevice), + proto.Success(), + proto.Features(), + ] + ) + self.client.set_input_flow(input_flow) + + # No PIN, no passphrase, don't display random + device.reset( + self.client, + display_random=False, + strength=strength, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + ) + + # generate mnemonic locally + internal_entropy = self.client.debug.state().reset_entropy + entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + # Compare that device generated proper mnemonic for given entropies + assert " ".join(words) == expected_mnemonic + + # Check if device is properly initialized + resp = self.client.call_raw(proto.Initialize()) + assert resp.initialized is True + assert resp.needs_backup is False + assert resp.pin_protection is False + assert resp.passphrase_protection is False + + def test_reset_device_pin(self): + words = [] + strength = 128 + + def input_flow(): + # Confirm Reset + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + # Enter new PIN + yield + self.client.debug.input("654") + + # Confirm PIN + yield + self.client.debug.input("654") + + # Confirm entropy + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + # Backup your seed + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + # mnemonic phrases + btn_code = yield + assert btn_code == B.ResetDevice + # 12 words, 3 pages + for i in range(3): + time.sleep(1) + words.extend(self.client.debug.state().reset_word.split()) + if i < 2: + self.client.debug.swipe_down() + else: + # last page is confirmation + self.client.debug.press_yes() + + # check backup words + for _ in range(2): + time.sleep(1) + index = self.client.debug.state().reset_word_pos + self.client.debug.input(words[index]) + + # safety warning + btn_code = yield + assert btn_code == B.ResetDevice + self.client.debug.press_yes() + + os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY) + with mock.patch("os.urandom", os_urandom), self.client: + self.client.set_expected_responses( + [ + proto.ButtonRequest(code=B.ResetDevice), + proto.ButtonRequest(code=B.Other), + proto.ButtonRequest(code=B.Other), + proto.ButtonRequest(code=B.ResetDevice), + proto.EntropyRequest(), + proto.ButtonRequest(code=B.ResetDevice), + proto.ButtonRequest(code=B.ResetDevice), + proto.ButtonRequest(code=B.ResetDevice), + proto.Success(), + proto.Features(), + ] + ) + self.client.set_input_flow(input_flow) + + # PIN, passphrase, display random + device.reset( + self.client, + display_random=True, + strength=strength, + passphrase_protection=True, + pin_protection=True, + label="test", + language="english", + ) + + # generate mnemonic locally + internal_entropy = self.client.debug.state().reset_entropy + entropy = generate_entropy(strength, internal_entropy, EXTERNAL_ENTROPY) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + # Compare that device generated proper mnemonic for given entropies + assert " ".join(words) == expected_mnemonic + + # Check if device is properly initialized + resp = self.client.call_raw(proto.Initialize()) + assert resp.initialized is True + assert resp.needs_backup is False + assert resp.pin_protection is True + assert resp.passphrase_protection is True + + def test_failed_pin(self): + # external_entropy = b'zlutoucky kun upel divoke ody' * 2 + strength = 128 + ret = self.client.call_raw( + proto.ResetDevice(strength=strength, pin_protection=True, label="test") + ) + + # Confirm Reset + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.press_yes() + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter PIN for first time + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.input("654") + ret = self.client.call_raw(proto.ButtonAck()) + + # Enter PIN for second time + assert isinstance(ret, proto.ButtonRequest) + self.client.debug.input("456") + ret = self.client.call_raw(proto.ButtonAck()) + + assert isinstance(ret, proto.ButtonRequest) + + def test_already_initialized(self): + self.setup_mnemonic_nopin_nopassphrase() + with pytest.raises(Exception): + device.reset(self.client, False, 128, True, True, "label", "english") diff --git a/python/trezorlib/tests/device_tests/test_msg_ripple_get_address.py b/python/trezorlib/tests/device_tests/test_msg_ripple_get_address.py new file mode 100644 index 000000000..6bdedda23 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ripple_get_address.py @@ -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 . + +import pytest + +from trezorlib import debuglink +from trezorlib.ripple import get_address +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.ripple +@pytest.mark.skip_t1 # T1 support is not planned +class TestMsgRippleGetAddress(TrezorTest): + def test_ripple_get_address(self): + # data from https://iancoleman.io/bip39/#english + self.setup_mnemonic_allallall() + + address = get_address(self.client, parse_path("m/44'/144'/0'/0/0")) + assert address == "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H" + address = get_address(self.client, parse_path("m/44'/144'/0'/0/1")) + assert address == "rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws" + address = get_address(self.client, parse_path("m/44'/144'/1'/0/0")) + assert address == "rJX2KwzaLJDyFhhtXKi3htaLfaUH2tptEX" + + def test_ripple_get_address_other(self): + # data from https://github.com/you21979/node-ripple-bip32/blob/master/test/test.js + debuglink.load_device_by_mnemonic( + self.client, + mnemonic="armed bundle pudding lazy strategy impulse where identify submit weekend physical antenna flight social acoustic absurd whip snack decide blur unfold fiction pumpkin athlete", + pin="", + passphrase_protection=False, + label="test", + language="english", + ) + address = get_address(self.client, parse_path("m/44'/144'/0'/0/0")) + assert address == "r4ocGE47gm4G4LkA9mriVHQqzpMLBTgnTY" + address = get_address(self.client, parse_path("m/44'/144'/0'/0/1")) + assert address == "rUt9ULSrUvfCmke8HTFU1szbmFpWzVbBXW" diff --git a/python/trezorlib/tests/device_tests/test_msg_ripple_sign_tx.py b/python/trezorlib/tests/device_tests/test_msg_ripple_sign_tx.py new file mode 100644 index 000000000..61b139d1b --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_ripple_sign_tx.py @@ -0,0 +1,118 @@ +# 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 . + +import pytest + +from trezorlib import messages, ripple +from trezorlib.tools import CallException, parse_path + +from .common import TrezorTest + + +@pytest.mark.ripple +@pytest.mark.skip_t1 # T1 support is not planned +class TestMsgRippleSignTx(TrezorTest): + def test_ripple_sign_simple_tx(self): + self.setup_mnemonic_allallall() + + msg = ripple.create_sign_tx_msg( + { + "TransactionType": "Payment", + "Payment": { + "Amount": 100000000, + "Destination": "rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws", + }, + "Flags": 0x80000000, + "Fee": 100000, + "Sequence": 25, + } + ) + resp = ripple.sign_tx(self.client, parse_path("m/44'/144'/0'/0/0"), msg) + assert ( + resp.signature.hex() + == "3045022100e243ef623675eeeb95965c35c3e06d63a9fc68bb37e17dc87af9c0af83ec057e02206ca8aa5eaab8396397aef6d38d25710441faf7c79d292ee1d627df15ad9346c0" + ) + assert ( + resp.serialized_tx.hex() + == "12000022800000002400000019614000000005f5e1006840000000000186a0732102131facd1eab748d6cddc492f54b04e8c35658894f4add2232ebc5afe7521dbe474473045022100e243ef623675eeeb95965c35c3e06d63a9fc68bb37e17dc87af9c0af83ec057e02206ca8aa5eaab8396397aef6d38d25710441faf7c79d292ee1d627df15ad9346c081148fb40e1ffa5d557ce9851a535af94965e0dd098883147148ebebf7304ccdf1676fefcf9734cf1e780826" + ) + + msg = ripple.create_sign_tx_msg( + { + "TransactionType": "Payment", + "Payment": { + "Amount": 1, + "Destination": "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H", + }, + "Fee": 10, + "Sequence": 1, + } + ) + resp = ripple.sign_tx(self.client, parse_path("m/44'/144'/0'/0/2"), msg) + assert ( + resp.signature.hex() + == "3044022069900e6e578997fad5189981b74b16badc7ba8b9f1052694033fa2779113ddc002206c8006ada310edf099fb22c0c12073550c8fc73247b236a974c5f1144831dd5f" + ) + assert ( + resp.serialized_tx.hex() + == "1200002280000000240000000161400000000000000168400000000000000a732103dbed1e77cb91a005e2ec71afbccce5444c9be58276665a3859040f692de8fed274463044022069900e6e578997fad5189981b74b16badc7ba8b9f1052694033fa2779113ddc002206c8006ada310edf099fb22c0c12073550c8fc73247b236a974c5f1144831dd5f8114bdf86f3ae715ba346b7772ea0e133f48828b766483148fb40e1ffa5d557ce9851a535af94965e0dd0988" + ) + + msg = ripple.create_sign_tx_msg( + { + "TransactionType": "Payment", + "Payment": { + "Amount": 100000009, + "Destination": "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H", + "DestinationTag": 123456, + }, + "Flags": 0, + "Fee": 100, + "Sequence": 100, + "LastLedgerSequence": 333111, + } + ) + resp = ripple.sign_tx(self.client, parse_path("m/44'/144'/0'/0/2"), msg) + assert ( + resp.signature.hex() + == "30450221008770743a472bb2d1c746a53ef131cc17cc118d538ec910ca928d221db4494cf702201e4ef242d6c3bff110c3cc3897a471fed0f5ac10987ea57da63f98dfa01e94df" + ) + assert ( + resp.serialized_tx.hex() + == "120000228000000024000000642e0001e240201b00051537614000000005f5e109684000000000000064732103dbed1e77cb91a005e2ec71afbccce5444c9be58276665a3859040f692de8fed2744730450221008770743a472bb2d1c746a53ef131cc17cc118d538ec910ca928d221db4494cf702201e4ef242d6c3bff110c3cc3897a471fed0f5ac10987ea57da63f98dfa01e94df8114bdf86f3ae715ba346b7772ea0e133f48828b766483148fb40e1ffa5d557ce9851a535af94965e0dd0988" + ) + + def test_ripple_sign_invalid_fee(self): + self.setup_mnemonic_allallall() + + msg = ripple.create_sign_tx_msg( + { + "TransactionType": "Payment", + "Payment": { + "Amount": 1, + "Destination": "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H", + }, + "Flags": 1, + "Fee": 1, + "Sequence": 1, + } + ) + with pytest.raises(CallException) as exc: + ripple.sign_tx(self.client, parse_path("m/44'/144'/0'/0/2"), msg) + assert exc.value.args[0] == messages.FailureType.ProcessError + assert exc.value.args[1].endswith( + "Fee must be in the range of 10 to 10,000 drops" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_signidentity.py b/python/trezorlib/tests/device_tests/test_msg_signidentity.py new file mode 100644 index 000000000..b1fcd4601 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_signidentity.py @@ -0,0 +1,137 @@ +# 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 . + +import struct + +from trezorlib import messages as proto, misc +from trezorlib.tools import H_ + +from .common import TrezorTest + + +def check_path(identity): + from hashlib import sha256 + + m = sha256() + m.update(struct.pack("