From 07ba960ab4aa5aa3ddf16ae74c3658782d491250 Mon Sep 17 00:00:00 2001 From: wszdexdrf Date: Mon, 5 Jun 2023 11:20:39 +0530 Subject: [PATCH] feat(rust): add trezor-client crate --- rust/trezor-client/.clippy.toml | 1 + rust/trezor-client/.gitignore | 1 + rust/trezor-client/Cargo.lock | 885 ++++++++++++++++++ rust/trezor-client/Cargo.toml | 57 ++ rust/trezor-client/LICENSE | 122 +++ rust/trezor-client/README.md | 43 + rust/trezor-client/examples/change_pin.rs | 31 + rust/trezor-client/examples/features.rs | 98 ++ rust/trezor-client/examples/find.rs | 11 + rust/trezor-client/examples/interaction.rs | 57 ++ rust/trezor-client/examples/sign_message.rs | 73 ++ rust/trezor-client/examples/sign_tx.rs | 119 +++ rust/trezor-client/rustfmt.toml | 11 + .../scripts/generate-messages.py | 108 +++ rust/trezor-client/scripts/generate-protos.sh | 16 + rust/trezor-client/src/client/bitcoin.rs | 95 ++ rust/trezor-client/src/client/common.rs | 247 +++++ rust/trezor-client/src/client/ethereum.rs | 167 ++++ rust/trezor-client/src/client/mod.rs | 241 +++++ rust/trezor-client/src/error.rs | 92 ++ rust/trezor-client/src/flows/sign_tx.rs | 334 +++++++ rust/trezor-client/src/lib.rs | 187 ++++ rust/trezor-client/src/messages.rs | 300 ++++++ rust/trezor-client/src/transport/error.rs | 49 + rust/trezor-client/src/transport/mod.rs | 85 ++ rust/trezor-client/src/transport/protocol.rs | 208 ++++ rust/trezor-client/src/transport/udp.rs | 153 +++ rust/trezor-client/src/transport/webusb.rs | 175 ++++ rust/trezor-client/src/utils.rs | 76 ++ 29 files changed, 4042 insertions(+) create mode 100644 rust/trezor-client/.clippy.toml create mode 100644 rust/trezor-client/.gitignore create mode 100644 rust/trezor-client/Cargo.lock create mode 100644 rust/trezor-client/Cargo.toml create mode 100644 rust/trezor-client/LICENSE create mode 100644 rust/trezor-client/README.md create mode 100644 rust/trezor-client/examples/change_pin.rs create mode 100644 rust/trezor-client/examples/features.rs create mode 100644 rust/trezor-client/examples/find.rs create mode 100644 rust/trezor-client/examples/interaction.rs create mode 100644 rust/trezor-client/examples/sign_message.rs create mode 100644 rust/trezor-client/examples/sign_tx.rs create mode 100644 rust/trezor-client/rustfmt.toml create mode 100755 rust/trezor-client/scripts/generate-messages.py create mode 100755 rust/trezor-client/scripts/generate-protos.sh create mode 100644 rust/trezor-client/src/client/bitcoin.rs create mode 100644 rust/trezor-client/src/client/common.rs create mode 100644 rust/trezor-client/src/client/ethereum.rs create mode 100644 rust/trezor-client/src/client/mod.rs create mode 100644 rust/trezor-client/src/error.rs create mode 100644 rust/trezor-client/src/flows/sign_tx.rs create mode 100644 rust/trezor-client/src/lib.rs create mode 100644 rust/trezor-client/src/messages.rs create mode 100644 rust/trezor-client/src/transport/error.rs create mode 100644 rust/trezor-client/src/transport/mod.rs create mode 100644 rust/trezor-client/src/transport/protocol.rs create mode 100644 rust/trezor-client/src/transport/udp.rs create mode 100644 rust/trezor-client/src/transport/webusb.rs create mode 100644 rust/trezor-client/src/utils.rs diff --git a/rust/trezor-client/.clippy.toml b/rust/trezor-client/.clippy.toml new file mode 100644 index 000000000..13f202e9e --- /dev/null +++ b/rust/trezor-client/.clippy.toml @@ -0,0 +1 @@ +msrv = "1.60" diff --git a/rust/trezor-client/.gitignore b/rust/trezor-client/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/rust/trezor-client/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/rust/trezor-client/Cargo.lock b/rust/trezor-client/Cargo.lock new file mode 100644 index 000000000..f2ad7aa28 --- /dev/null +++ b/rust/trezor-client/Cargo.lock @@ -0,0 +1,885 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitcoin" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36f4c848f6bd9ff208128f08751135846cc23ae57d66ab10a22efff1c675f3c" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "rusb" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a8c36914f9b1a3be712c1dfa48c9b397131f9a75707e570a391735f785c5d1" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trezor-client" +version = "0.1.0" +dependencies = [ + "bitcoin", + "byteorder", + "hex", + "primitive-types", + "protobuf", + "protobuf-codegen", + "rusb", + "serial_test", + "thiserror", + "tracing", + "tracing-subscriber", + "unicode-normalization", +] + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/rust/trezor-client/Cargo.toml b/rust/trezor-client/Cargo.toml new file mode 100644 index 000000000..7d39f3f5e --- /dev/null +++ b/rust/trezor-client/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "trezor-client" +version = "0.1.0" +authors = [ + "joshie", + "DaniPopes <57450786+DaniPopes@users.noreply.github.com>", + "romanz", + "Steven Roose ", +] +license = "CC0-1.0" +homepage = "https://github.com/joshieDo/rust-trezor-api" +repository = "https://github.com/joshieDo/rust-trezor-api" +description = "Client library for interfacing with Trezor hardware wallet devices" +keywords = ["ethereum", "bitcoin", "trezor", "wallet"] +categories = ["api-bindings", "cryptography::cryptocurrencies"] +readme = "README.md" +exclude = [".github/", ".vscode/", "examples/", "scripts/", "trezor-common/", "rustfmt.toml"] +edition = "2021" +rust-version = "1.60" + +[dependencies] +protobuf = "3.2" +byteorder = "1.4" +rusb = "0.9" + +hex = { version = "0.4", default-features = false, features = ["std"] } +thiserror = "1.0" +tracing = "0.1" + +# bitcoin +bitcoin = { version = "0.30", optional = true } +unicode-normalization = { version = "0.1.22", optional = true } + +# ethereum +primitive-types = { version = "0.12", default-features = false, optional = true } + +[dev-dependencies] +tracing-subscriber = "0.3" +serial_test = "2.0.0" + +[features] +default = ["bitcoin", "ethereum"] + +# Client implementations +bitcoin = ["dep:bitcoin", "unicode-normalization"] +ethereum = ["primitive-types"] + +# Just bindings to the Trezor protobufs +binance = [] +cardano = [] +eos = [] +monero = [] +nem = [] +ripple = [] +stellar = [] +tezos = [] +webauthn = [] diff --git a/rust/trezor-client/LICENSE b/rust/trezor-client/LICENSE new file mode 100644 index 000000000..6ca207ef0 --- /dev/null +++ b/rust/trezor-client/LICENSE @@ -0,0 +1,122 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + diff --git a/rust/trezor-client/README.md b/rust/trezor-client/README.md new file mode 100644 index 000000000..e347af31c --- /dev/null +++ b/rust/trezor-client/README.md @@ -0,0 +1,43 @@ +# trezor-client + +[![Downloads][downloads-badge]][crates-io] +[![License][license-badge]][license-url] +[![CI Status][actions-badge]][actions-url] + +A fork of a [fork](https://github.com/romanz/rust-trezor-api) of a [library](https://github.com/stevenroose/rust-trezor-api) that provides a way to communicate with a Trezor T device from a Rust project. + +Previous iterations provided implementations for Bitcoin only. **This crate also provides an Ethereum interface**, mainly for use in [ethers-rs](https://github.com/gakonst/ethers-rs/). + +## Requirements + +**MSRV: 1.60** + +See the [Trezor guide](https://trezor.io/learn/a/os-requirements-for-trezor) on how to install and use the Trezor Suite app. + +Last tested with firmware v2.4.2. + +## Examples / Tests + +`cargo run --example features` + +## Features + +- `bitcoin` and `ethereum`: client implementation and full support; +- `cardano`, `lisk`, `monero`, `nem`, `ontology`, `ripple`, `stellar`, `tezos`, and`tron`: only protobuf bindings. + +## Future + +At the moment, not looking into expanding more than what's necessary to maintain compatability/usability with ethers-rs. + +## Credits + +- [Trezor](https://github.com/trezor/trezor-firmware) +- [stevenroose](https://github.com/stevenroose) +- [romanz](https://github.com/romanz) + +[downloads-badge]: https://img.shields.io/crates/d/trezor-client?style=for-the-badge&logo=rust +[crates-io]: https://crates.io/crates/trezor-client +[license-badge]: https://img.shields.io/badge/license-CC0--1.0-blue.svg?style=for-the-badge +[license-url]: https://github.com/joshieDo/rust-trezor-api/blob/master/LICENSE +[actions-badge]: https://img.shields.io/github/actions/workflow/status/joshieDo/rust-trezor-api/ci.yml?branch=master&style=for-the-badge +[actions-url]: https://github.com/joshieDo/rust-trezor-api/actions?query=workflow%3ACI+branch%3Amaster diff --git a/rust/trezor-client/examples/change_pin.rs b/rust/trezor-client/examples/change_pin.rs new file mode 100644 index 000000000..59ed37257 --- /dev/null +++ b/rust/trezor-client/examples/change_pin.rs @@ -0,0 +1,31 @@ +use std::io; + +fn read_pin() -> String { + println!("Enter PIN"); + let mut pin = String::new(); + if io::stdin().read_line(&mut pin).unwrap() != 5 { + println!("must enter pin, received: {}", pin); + } + // trim newline + pin[..4].to_owned() +} + +fn do_main() -> Result<(), trezor_client::Error> { + // init with debugging + let mut trezor = trezor_client::unique(true)?; + trezor.init_device(None)?; + + let old_pin = trezor.change_pin(false)?.button_request()?.ack()?.pin_matrix_request()?; + + let new_pin1 = old_pin.ack_pin(read_pin())?.pin_matrix_request()?; + + let new_pin2 = new_pin1.ack_pin(read_pin())?.pin_matrix_request()?; + + new_pin2.ack_pin(read_pin())?.ok()?; + + Ok(()) +} + +fn main() { + do_main().unwrap() +} diff --git a/rust/trezor-client/examples/features.rs b/rust/trezor-client/examples/features.rs new file mode 100644 index 000000000..dc95d3501 --- /dev/null +++ b/rust/trezor-client/examples/features.rs @@ -0,0 +1,98 @@ +use std::io; + +fn convert_path_from_str(derivation: &str) -> Vec { + let derivation = derivation.to_string(); + let elements = derivation.split('/').skip(1).collect::>(); + + let mut path = vec![]; + for derivation_index in elements { + let hardened = derivation_index.contains('\''); + let mut index = derivation_index.replace('\'', "").parse::().unwrap(); + if hardened { + index |= 0x80000000; + } + path.push(index); + } + + path +} + +fn device_selector() -> trezor_client::Trezor { + let mut devices = trezor_client::find_devices(false); + + if devices.is_empty() { + panic!("No devices connected"); + } else if devices.len() == 1 { + devices.remove(0).connect().expect("connection error") + } else { + println!("Choose device:"); + for (i, dev) in devices.iter().enumerate() { + println!("{}: {}", i + 1, dev); + } + println!("Enter device number: "); + let mut inp = String::new(); + io::stdin().read_line(&mut inp).expect("stdin error"); + let idx: usize = inp[..inp.len() - 1].parse().expect("invalid number"); + if idx >= devices.len() { + panic!("Index out of range"); + } + devices.remove(idx).connect().unwrap() + } +} + +fn do_main() -> Result<(), trezor_client::Error> { + // init with debugging + tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init(); + + let mut trezor = device_selector(); + trezor.init_device(None)?; + let f = trezor.features().expect("no features").clone(); + + println!("Features:"); + println!("vendor: {}", f.vendor()); + println!("version: {}.{}.{}", f.major_version(), f.minor_version(), f.patch_version()); + println!("device id: {}", f.device_id()); + println!("label: {}", f.label()); + println!("is initialized: {}", f.initialized()); + println!("pin protection: {}", f.pin_protection()); + println!("passphrase protection: {}", f.passphrase_protection()); + println!( + "{:?}", + trezor.ethereum_get_address(convert_path_from_str("m/44'/60'/1'/0/0")).unwrap() + ); + drop(trezor); + + let mut trezor2 = device_selector(); + + trezor2.init_device(Some(f.session_id().to_vec()))?; + + println!( + "{:?}", + trezor2.ethereum_get_address(convert_path_from_str("m/44'/60'/1'/0/0")).unwrap() + ); + //optional bool bootloader_mode = 5; // is device in bootloader mode? + //optional string language = 9; // device language + //optional bytes revision = 13; // SCM revision of firmware + //optional bytes bootloader_hash = 14; // hash of the bootloader + //optional bool imported = 15; // was storage imported from an external source? + //optional bool pin_cached = 16; // is PIN already cached in session? + //optional bool passphrase_cached = 17; // is passphrase already cached in session? + //optional bool firmware_present = 18; // is valid firmware loaded? + //optional bool needs_backup = 19; // does storage need backup? (equals to + // Storage.needs_backup) optional uint32 flags = 20; // device flags (equals + // to Storage.flags) optional string model = 21; // device hardware model + //optional uint32 fw_major = 22; // reported firmware version if in bootloader + // mode optional uint32 fw_minor = 23; // reported firmware version if in + // bootloader mode optional uint32 fw_patch = 24; // reported firmware version + // if in bootloader mode optional string fw_vendor = 25; // reported firmware + // vendor if in bootloader mode optional bytes fw_vendor_keys = 26; // reported + // firmware vendor keys (their hash) optional bool unfinished_backup = 27; // report + // unfinished backup (equals to Storage.unfinished_backup) optional bool no_backup = 28; + // // report no backup (equals to Storage.no_backup) + + Ok(()) +} + +fn main() { + do_main().unwrap() +} diff --git a/rust/trezor-client/examples/find.rs b/rust/trezor-client/examples/find.rs new file mode 100644 index 000000000..b538fa95d --- /dev/null +++ b/rust/trezor-client/examples/find.rs @@ -0,0 +1,11 @@ +fn main() { + let trezors = trezor_client::find_devices(false); + println!("Found {} devices: ", trezors.len()); + for t in trezors.into_iter() { + println!("- {}", t); + { + let mut client = t.connect().unwrap(); + println!("{:?}", client.initialize(None).unwrap()); + } + } +} diff --git a/rust/trezor-client/examples/interaction.rs b/rust/trezor-client/examples/interaction.rs new file mode 100644 index 000000000..8eb1c687f --- /dev/null +++ b/rust/trezor-client/examples/interaction.rs @@ -0,0 +1,57 @@ +use std::io; + +use bitcoin::{bip32, network::constants::Network, Address}; +use trezor_client::{Error, TrezorMessage, TrezorResponse}; + +fn handle_interaction(resp: TrezorResponse) -> Result { + match resp { + TrezorResponse::Ok(res) => Ok(res), + TrezorResponse::Failure(_) => resp.ok(), // assering ok() returns the failure error + TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack()?), + TrezorResponse::PinMatrixRequest(req) => { + println!("Enter PIN"); + let mut pin = String::new(); + if io::stdin().read_line(&mut pin).unwrap() != 5 { + println!("must enter pin, received: {}", pin); + } + // trim newline + handle_interaction(req.ack_pin(pin[..4].to_owned())?) + } + TrezorResponse::PassphraseRequest(req) => { + println!("Enter passphrase"); + let mut pass = String::new(); + io::stdin().read_line(&mut pass).unwrap(); + // trim newline + handle_interaction(req.ack_passphrase(pass[..pass.len() - 1].to_owned())?) + } + } +} + +fn do_main() -> Result<(), trezor_client::Error> { + // init with debugging + let mut trezor = trezor_client::unique(true)?; + trezor.init_device(None)?; + + let xpub = handle_interaction( + trezor.get_public_key( + &vec![ + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + ] + .into(), + trezor_client::protos::InputScriptType::SPENDADDRESS, + Network::Testnet, + true, + )?, + )?; + println!("{}", xpub); + println!("{:?}", xpub); + println!("{}", Address::p2pkh(&xpub.to_pub(), Network::Testnet)); + + Ok(()) +} + +fn main() { + do_main().unwrap() +} diff --git a/rust/trezor-client/examples/sign_message.rs b/rust/trezor-client/examples/sign_message.rs new file mode 100644 index 000000000..c10e237f2 --- /dev/null +++ b/rust/trezor-client/examples/sign_message.rs @@ -0,0 +1,73 @@ +use std::io; + +use bitcoin::{bip32, network::constants::Network, Address}; +use trezor_client::{InputScriptType, TrezorMessage, TrezorResponse}; + +fn handle_interaction(resp: TrezorResponse) -> T { + match resp { + TrezorResponse::Ok(res) => res, + // assering ok() returns the failure error + TrezorResponse::Failure(_) => resp.ok().unwrap(), + TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack().unwrap()), + TrezorResponse::PinMatrixRequest(req) => { + println!("Enter PIN"); + let mut pin = String::new(); + if io::stdin().read_line(&mut pin).unwrap() != 5 { + println!("must enter pin, received: {}", pin); + } + // trim newline + handle_interaction(req.ack_pin(pin[..4].to_owned()).unwrap()) + } + TrezorResponse::PassphraseRequest(req) => { + println!("Enter passphrase"); + let mut pass = String::new(); + io::stdin().read_line(&mut pass).unwrap(); + // trim newline + handle_interaction(req.ack_passphrase(pass[..pass.len() - 1].to_owned()).unwrap()) + } + } +} + +fn main() { + tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init(); + + // init with debugging + let mut trezor = trezor_client::unique(false).unwrap(); + trezor.init_device(None).unwrap(); + + let pubkey = handle_interaction( + trezor + .get_public_key( + &vec![ + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(1).unwrap(), + ] + .into(), + trezor_client::protos::InputScriptType::SPENDADDRESS, + Network::Testnet, + true, + ) + .unwrap(), + ); + let addr = Address::p2pkh(&pubkey.to_pub(), Network::Testnet); + println!("address: {}", addr); + + let (addr, signature) = handle_interaction( + trezor + .sign_message( + "regel het".to_owned(), + &vec![ + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(1).unwrap(), + ] + .into(), + InputScriptType::SPENDADDRESS, + Network::Testnet, + ) + .unwrap(), + ); + println!("Addr from device: {}", addr); + println!("Signature: {:?}", signature); +} diff --git a/rust/trezor-client/examples/sign_tx.rs b/rust/trezor-client/examples/sign_tx.rs new file mode 100644 index 000000000..254539681 --- /dev/null +++ b/rust/trezor-client/examples/sign_tx.rs @@ -0,0 +1,119 @@ +use std::io::{self, Write}; + +use bitcoin::{ + bip32, blockdata::script::Builder, consensus::encode::Decodable, network::constants::Network, + psbt, Address, Sequence, Transaction, TxIn, TxOut, +}; + +use trezor_client::{Error, SignTxProgress, TrezorMessage, TrezorResponse}; + +fn handle_interaction(resp: TrezorResponse) -> T { + match resp { + TrezorResponse::Ok(res) => res, + // assering ok() returns the failure error + TrezorResponse::Failure(_) => resp.ok().unwrap(), + TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack().unwrap()), + TrezorResponse::PinMatrixRequest(req) => { + println!("Enter PIN"); + let mut pin = String::new(); + if io::stdin().read_line(&mut pin).unwrap() != 5 { + println!("must enter pin, received: {}", pin); + } + // trim newline + handle_interaction(req.ack_pin(pin[..4].to_owned()).unwrap()) + } + TrezorResponse::PassphraseRequest(req) => { + println!("Enter passphrase"); + let mut pass = String::new(); + io::stdin().read_line(&mut pass).unwrap(); + // trim newline + handle_interaction(req.ack_passphrase(pass[..pass.len() - 1].to_owned()).unwrap()) + } + } +} + +fn tx_progress( + psbt: &mut psbt::PartiallySignedTransaction, + progress: SignTxProgress, + raw_tx: &mut Vec, +) -> Result<(), Error> { + if let Some(part) = progress.get_serialized_tx_part() { + raw_tx.write_all(part).unwrap(); + } + + if !progress.finished() { + let progress = handle_interaction(progress.ack_psbt(psbt, Network::Testnet).unwrap()); + tx_progress(psbt, progress, raw_tx) + } else { + Ok(()) + } +} + +fn main() { + tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init(); + + // init with debugging + let mut trezor = trezor_client::unique(false).unwrap(); + trezor.init_device(None).unwrap(); + + let pubkey = handle_interaction( + trezor + .get_public_key( + &vec![ + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(1).unwrap(), + ] + .into(), + trezor_client::protos::InputScriptType::SPENDADDRESS, + Network::Testnet, + true, + ) + .unwrap(), + ); + let addr = Address::p2pkh(&pubkey.to_pub(), Network::Testnet); + println!("address: {}", addr); + + let mut psbt = psbt::PartiallySignedTransaction { + unsigned_tx: Transaction { + version: 1, + lock_time: bitcoin::absolute::LockTime::from_consensus(0), + input: vec![TxIn { + previous_output: "c5bdb27907b78ce03f94e4bf2e94f7a39697b9074b79470019e3dbc76a10ecb6:0".parse().unwrap(), + sequence: Sequence(0xffffffff), + script_sig: Builder::new().into_script(), + witness: Default::default(), + }], + output: vec![TxOut { + value: 14245301, + script_pubkey: addr.script_pubkey(), + }], + }, + inputs: vec![psbt::Input { + non_witness_utxo: Some(Transaction::consensus_decode(&mut &hex::decode("020000000001011eb5a3e65946f88b00d67b321e5fd980b32a2316fb1fc9b712baa6a1033a04e30100000017160014f0f81ee77d552b4c81497451d1abf5c22ce8e352feffffff02b55dd900000000001976a9142c3cf5686f47c1de9cc90b4255cc2a1ef8c01b3188acfb0391ae6800000017a914a3a79e37ad366d9bf9471b28a9a8f64b50de0c968702483045022100c0aa7b262967fc2803c8a9f38f26682edba7cafb7d4870ebdc116040ad5338b502205dfebd08e993af2e6aa3118a438ad70ed9f6e09bc6abfd21f8f2957af936bc070121031f4e69fcf110bb31f019321834c0948b5487f2782489f370f66dc20f7ac767ca8bf81500").unwrap()[..]).unwrap()), + ..Default::default() + }], + outputs: vec![ + psbt::Output { + ..Default::default() + }, + ], + proprietary: Default::default(), + unknown: Default::default(), + version: 0, + xpub: Default::default(), + }; + + println!("psbt before: {:?}", psbt); + println!("unsigned txid: {}", psbt.unsigned_tx.txid()); + println!( + "unsigned tx: {}", + hex::encode(bitcoin::consensus::encode::serialize(&psbt.unsigned_tx)) + ); + + let mut raw_tx = Vec::new(); + let progress = handle_interaction(trezor.sign_tx(&psbt, Network::Testnet).unwrap()); + tx_progress(&mut psbt, progress, &mut raw_tx).unwrap(); + + println!("signed tx: {}", hex::encode(raw_tx)); +} diff --git a/rust/trezor-client/rustfmt.toml b/rust/trezor-client/rustfmt.toml new file mode 100644 index 000000000..422c4f323 --- /dev/null +++ b/rust/trezor-client/rustfmt.toml @@ -0,0 +1,11 @@ +reorder_imports = true +imports_granularity = "Crate" +use_small_heuristics = "Max" +comment_width = 100 +wrap_comments = true +binop_separator = "Back" +trailing_comma = "Vertical" +trailing_semicolon = false +use_field_init_shorthand = true + +ignore = ["src/protos/messages_*"] diff --git a/rust/trezor-client/scripts/generate-messages.py b/rust/trezor-client/scripts/generate-messages.py new file mode 100755 index 000000000..ced30e127 --- /dev/null +++ b/rust/trezor-client/scripts/generate-messages.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# Generates the `trezor_message_impl!` macro calls for the `src/messages.rs` file. + +from os import path + +# Path to the `messages.proto` file +PATH = path.abspath(path.join(__file__, "../../../../common/protob/messages.proto")) +# Prefix of the enum variants +PREFIX = "MessageType_" +# Mapping of block name to feature name +FEATURES = { + # no features + "Management": "default", + "Bootloader": "default", + "Crypto": "default", + "Debug": "default", + # + "Bitcoin": "bitcoin", + "Ethereum": "ethereum", + # + "Binance": "binance", + "Cardano": "cardano", + "EOS": "eos", + "Monero": "monero", + "NEM": "nem", + "Ripple": "ripple", + "Stellar": "stellar", + "Tezos": "tezos", + "WebAuthn": "webauthn", +} +MACRO = "trezor_message_impl" +INDENT = "\t" + + +def main(): + blocks = get_blocks() + features = {} + defaults = [] + for block, variants in blocks.items(): + f = FEATURES.get(block) + if not f or f == "default": + defaults.extend(variants) + else: + vs = features.get(f) + if vs: + vs.extend(variants) + else: + features[f] = variants + + items = list(features.items()) + items.sort() + + out = write_block(defaults) + for feature, variants in items: + if variants and feature: + out += "\n" + out += write_block(variants, feature) + print(out) + + +# Parse feature blocks based on comments in the `messages.proto` file +def get_blocks() -> dict[str, list[str]]: + blocks = {} + current_block = "" + with open(PATH, "r") as file: + in_enum = False + in_block_comment = False + for line in file: + line = line.strip() + + if "/*" in line: + in_block_comment = True + if "*/" in line: + in_block_comment = False + if in_block_comment: + continue + + if line.startswith("enum MessageType {"): + in_enum = True + continue + if in_enum: + if line == "}": + break + if line.startswith("//"): + comment = line.removeprefix("//").strip() + if comment[0].isupper() and len(comment.split(" ")) == 1: + current_block = comment + blocks[current_block] = [] + elif line.startswith(PREFIX): + blocks[current_block].append(line.split(" ")[0]) + return blocks + + +# Writes a macro block +def write_block(variants: list[str], feature: str = "") -> str: + s = "" + if feature: + s += f'#[cfg(feature = "{feature}")]\n' + s += f"{MACRO}! {{\n" + for variant in variants: + s += f"{INDENT}{variant.removeprefix(PREFIX)} => {variant},\n" + s += "}\n" + return s + + +if __name__ == "__main__": + main() diff --git a/rust/trezor-client/scripts/generate-protos.sh b/rust/trezor-client/scripts/generate-protos.sh new file mode 100755 index 000000000..68f195e4b --- /dev/null +++ b/rust/trezor-client/scripts/generate-protos.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Generates src/protos/ +# Requires the `protoc-gen-rust` binary (`cargo install protoc-gen-rust`). +# Overwrites src/protos/mod.rs, but the change should not be committed, and +# instead should be handled manually. + +crate_root="$(dirname "$(dirname "$(realpath "$0")")")" +main_root="$(dirname $(dirname "$crate_root"))" +out_dir="$crate_root/src/protos" +proto_dir="$main_root/common/protob" + +protoc \ + --proto_path "$proto_dir" \ + --rust_out "$out_dir" \ + "$proto_dir"/*.proto diff --git a/rust/trezor-client/src/client/bitcoin.rs b/rust/trezor-client/src/client/bitcoin.rs new file mode 100644 index 000000000..592c0c137 --- /dev/null +++ b/rust/trezor-client/src/client/bitcoin.rs @@ -0,0 +1,95 @@ +use super::{Trezor, TrezorResponse}; +use crate::{error::Result, flows::sign_tx::SignTxProgress, protos, utils}; +use bitcoin::{ + address::NetworkUnchecked, bip32, network::constants::Network, psbt, + secp256k1::ecdsa::RecoverableSignature, Address, +}; + +pub use crate::protos::InputScriptType; + +impl Trezor { + pub fn get_public_key( + &mut self, + path: &bip32::DerivationPath, + script_type: InputScriptType, + network: Network, + show_display: bool, + ) -> Result> { + let mut req = protos::GetPublicKey::new(); + req.address_n = utils::convert_path(path); + req.set_show_display(show_display); + req.set_coin_name(utils::coin_name(network)?); + req.set_script_type(script_type); + self.call(req, Box::new(|_, m| Ok(m.xpub().parse()?))) + } + + //TODO(stevenroose) multisig + pub fn get_address( + &mut self, + path: &bip32::DerivationPath, + script_type: InputScriptType, + network: Network, + show_display: bool, + ) -> Result> { + let mut req = protos::GetAddress::new(); + req.address_n = utils::convert_path(path); + req.set_coin_name(utils::coin_name(network)?); + req.set_show_display(show_display); + req.set_script_type(script_type); + self.call(req, Box::new(|_, m| parse_address(m.address()))) + } + + pub fn sign_tx( + &mut self, + psbt: &psbt::PartiallySignedTransaction, + network: Network, + ) -> Result, protos::TxRequest>> { + let tx = &psbt.unsigned_tx; + let mut req = protos::SignTx::new(); + req.set_inputs_count(tx.input.len() as u32); + req.set_outputs_count(tx.output.len() as u32); + req.set_coin_name(utils::coin_name(network)?); + req.set_version(tx.version as u32); + req.set_lock_time(tx.lock_time.to_consensus_u32()); + self.call(req, Box::new(|c, m| Ok(SignTxProgress::new(c, m)))) + } + + pub fn sign_message( + &mut self, + message: String, + path: &bip32::DerivationPath, + script_type: InputScriptType, + network: Network, + ) -> Result> { + let mut req = protos::SignMessage::new(); + req.address_n = utils::convert_path(path); + // Normalize to Unicode NFC. + let msg_bytes = nfc_normalize(&message).into_bytes(); + req.set_message(msg_bytes); + req.set_coin_name(utils::coin_name(network)?); + req.set_script_type(script_type); + self.call( + req, + Box::new(|_, m| { + let address = parse_address(m.address())?; + let signature = utils::parse_recoverable_signature(m.signature())?; + Ok((address, signature)) + }), + ) + } +} + +fn parse_address(s: &str) -> Result
{ + let address = s.parse::>()?; + Ok(address.assume_checked()) +} + +// Modified from: +// https://github.com/rust-lang/rust/blob/2a8221dbdfd180a2d56d4b0089f4f3952d8c2bcd/compiler/rustc_parse/src/lexer/mod.rs#LL754C5-L754C5 +fn nfc_normalize(string: &str) -> String { + use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization}; + match is_nfc_quick(string.chars()) { + IsNormalized::Yes => string.to_string(), + _ => string.chars().nfc().collect::(), + } +} diff --git a/rust/trezor-client/src/client/common.rs b/rust/trezor-client/src/client/common.rs new file mode 100644 index 000000000..b8bef6c9f --- /dev/null +++ b/rust/trezor-client/src/client/common.rs @@ -0,0 +1,247 @@ +use crate::{ + error::{Error, Result}, + messages::TrezorMessage, + protos, Trezor, +}; +use std::fmt; + +// Some types with raw protos that we use in the public interface so they have to be exported. +pub use protos::{ + button_request::ButtonRequestType, pin_matrix_request::PinMatrixRequestType, Features, +}; + +#[cfg(feature = "bitcoin")] +pub use protos::InputScriptType; + +/// The different options for the number of words in a seed phrase. +pub enum WordCount { + W12 = 12, + W18 = 18, + W24 = 24, +} + +/// The different types of user interactions the Trezor device can request. +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum InteractionType { + Button, + PinMatrix, + Passphrase, + PassphraseState, +} + +//TODO(stevenroose) should this be FnOnce and put in an FnBox? +/// Function to be passed to the `Trezor.call` method to process the Trezor response message into a +/// general-purpose type. +pub type ResultHandler<'a, T, R> = dyn Fn(&'a mut Trezor, R) -> Result; + +/// A button request message sent by the device. +pub struct ButtonRequest<'a, T, R: TrezorMessage> { + pub message: protos::ButtonRequest, + pub client: &'a mut Trezor, + pub result_handler: Box>, +} + +impl<'a, T, R: TrezorMessage> fmt::Debug for ButtonRequest<'a, T, R> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.message, f) + } +} + +impl<'a, T, R: TrezorMessage> ButtonRequest<'a, T, R> { + /// The type of button request. + pub fn request_type(&self) -> ButtonRequestType { + self.message.code() + } + + /// Ack the request and get the next message from the device. + pub fn ack(self) -> Result> { + let req = protos::ButtonAck::new(); + self.client.call(req, self.result_handler) + } +} + +/// A PIN matrix request message sent by the device. +pub struct PinMatrixRequest<'a, T, R: TrezorMessage> { + pub message: protos::PinMatrixRequest, + pub client: &'a mut Trezor, + pub result_handler: Box>, +} + +impl<'a, T, R: TrezorMessage> fmt::Debug for PinMatrixRequest<'a, T, R> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.message, f) + } +} + +impl<'a, T, R: TrezorMessage> PinMatrixRequest<'a, T, R> { + /// The type of PIN matrix request. + pub fn request_type(&self) -> PinMatrixRequestType { + self.message.type_() + } + + /// Ack the request with a PIN and get the next message from the device. + pub fn ack_pin(self, pin: String) -> Result> { + let mut req = protos::PinMatrixAck::new(); + req.set_pin(pin); + self.client.call(req, self.result_handler) + } +} + +/// A passphrase request message sent by the device. +pub struct PassphraseRequest<'a, T, R: TrezorMessage> { + pub message: protos::PassphraseRequest, + pub client: &'a mut Trezor, + pub result_handler: Box>, +} + +impl<'a, T, R: TrezorMessage> fmt::Debug for PassphraseRequest<'a, T, R> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.message, f) + } +} + +impl<'a, T, R: TrezorMessage> PassphraseRequest<'a, T, R> { + /// Check whether the use is supposed to enter the passphrase on the device or not. + pub fn on_device(&self) -> bool { + self.message._on_device() + } + + /// Ack the request with a passphrase and get the next message from the device. + pub fn ack_passphrase(self, passphrase: String) -> Result> { + let mut req = protos::PassphraseAck::new(); + req.set_passphrase(passphrase); + self.client.call(req, self.result_handler) + } + + /// Ack the request without a passphrase to let the user enter it on the device + /// and get the next message from the device. + pub fn ack(self, on_device: bool) -> Result> { + let mut req = protos::PassphraseAck::new(); + if on_device { + req.set_on_device(on_device); + } + self.client.call(req, self.result_handler) + } +} + +/// A response from a Trezor device. On every message exchange, instead of the expected/desired +/// response, the Trezor can ask for some user interaction, or can send a failure. +#[derive(Debug)] +pub enum TrezorResponse<'a, T, R: TrezorMessage> { + Ok(T), + Failure(protos::Failure), + ButtonRequest(ButtonRequest<'a, T, R>), + PinMatrixRequest(PinMatrixRequest<'a, T, R>), + PassphraseRequest(PassphraseRequest<'a, T, R>), + //TODO(stevenroose) This should be taken out of this enum and intrinsically attached to the + // PassphraseRequest variant. However, it's currently impossible to do this. It might be + // possible to do with FnBox (currently nightly) or when Box becomes possible. +} + +impl<'a, T, R: TrezorMessage> fmt::Display for TrezorResponse<'a, T, R> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // TODO(stevenroose) should we make T: Debug? + TrezorResponse::Ok(ref _m) => f.write_str("Ok"), + TrezorResponse::Failure(ref m) => write!(f, "Failure: {:?}", m), + TrezorResponse::ButtonRequest(ref r) => write!(f, "ButtonRequest: {:?}", r), + TrezorResponse::PinMatrixRequest(ref r) => write!(f, "PinMatrixRequest: {:?}", r), + TrezorResponse::PassphraseRequest(ref r) => write!(f, "PassphraseRequest: {:?}", r), + } + } +} + +impl<'a, T, R: TrezorMessage> TrezorResponse<'a, T, R> { + /// Get the actual `Ok` response value or an error if not `Ok`. + pub fn ok(self) -> Result { + match self { + TrezorResponse::Ok(m) => Ok(m), + TrezorResponse::Failure(m) => Err(Error::FailureResponse(m)), + TrezorResponse::ButtonRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Button)) + } + TrezorResponse::PinMatrixRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::PinMatrix)) + } + TrezorResponse::PassphraseRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Passphrase)) + } + } + } + + /// Get the button request object or an error if not `ButtonRequest`. + pub fn button_request(self) -> Result> { + match self { + TrezorResponse::ButtonRequest(r) => Ok(r), + TrezorResponse::Ok(_) => Err(Error::UnexpectedMessageType(R::MESSAGE_TYPE)), + TrezorResponse::Failure(m) => Err(Error::FailureResponse(m)), + TrezorResponse::PinMatrixRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::PinMatrix)) + } + TrezorResponse::PassphraseRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Passphrase)) + } + } + } + + /// Get the PIN matrix request object or an error if not `PinMatrixRequest`. + pub fn pin_matrix_request(self) -> Result> { + match self { + TrezorResponse::PinMatrixRequest(r) => Ok(r), + TrezorResponse::Ok(_) => Err(Error::UnexpectedMessageType(R::MESSAGE_TYPE)), + TrezorResponse::Failure(m) => Err(Error::FailureResponse(m)), + TrezorResponse::ButtonRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Button)) + } + TrezorResponse::PassphraseRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Passphrase)) + } + } + } + + /// Get the passphrase request object or an error if not `PassphraseRequest`. + pub fn passphrase_request(self) -> Result> { + match self { + TrezorResponse::PassphraseRequest(r) => Ok(r), + TrezorResponse::Ok(_) => Err(Error::UnexpectedMessageType(R::MESSAGE_TYPE)), + TrezorResponse::Failure(m) => Err(Error::FailureResponse(m)), + TrezorResponse::ButtonRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::Button)) + } + TrezorResponse::PinMatrixRequest(_) => { + Err(Error::UnexpectedInteractionRequest(InteractionType::PinMatrix)) + } + } + } +} + +pub fn handle_interaction(resp: TrezorResponse<'_, T, R>) -> Result { + match resp { + TrezorResponse::Ok(res) => Ok(res), + TrezorResponse::Failure(_) => resp.ok(), // assering ok() returns the failure error + TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack()?), + TrezorResponse::PinMatrixRequest(_) => Err(Error::UnsupportedNetwork), + TrezorResponse::PassphraseRequest(req) => handle_interaction({ + let on_device = req.on_device(); + req.ack(!on_device)? + }), + } +} + +/// When resetting the device, it will ask for entropy to aid key generation. +pub struct EntropyRequest<'a> { + pub client: &'a mut Trezor, +} + +impl<'a> EntropyRequest<'a> { + /// Provide exactly 32 bytes or entropy. + pub fn ack_entropy(self, entropy: Vec) -> Result> { + if entropy.len() != 32 { + return Err(Error::InvalidEntropy) + } + + let mut req = protos::EntropyAck::new(); + req.set_entropy(entropy); + self.client.call(req, Box::new(|_, _| Ok(()))) + } +} diff --git a/rust/trezor-client/src/client/ethereum.rs b/rust/trezor-client/src/client/ethereum.rs new file mode 100644 index 000000000..2ee535c25 --- /dev/null +++ b/rust/trezor-client/src/client/ethereum.rs @@ -0,0 +1,167 @@ +use super::{handle_interaction, Trezor}; +use crate::{ + error::Result, + protos::{self, ethereum_sign_tx_eip1559::EthereumAccessList}, +}; +use primitive_types::U256; + +/// Access list item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccessListItem { + /// Accessed address + pub address: String, + /// Accessed storage keys + pub storage_keys: Vec>, +} + +/// An ECDSA signature. +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub struct Signature { + /// R value + pub r: U256, + /// S Value + pub s: U256, + /// V value in 'Electrum' notation. + pub v: u64, +} + +impl Trezor { + // ETHEREUM + pub fn ethereum_get_address(&mut self, path: Vec) -> Result { + let mut req = protos::EthereumGetAddress::new(); + req.address_n = path; + let address = handle_interaction( + self.call(req, Box::new(|_, m: protos::EthereumAddress| Ok(m.address().into())))?, + )?; + Ok(address) + } + + pub fn ethereum_sign_message(&mut self, message: Vec, path: Vec) -> Result { + let mut req = protos::EthereumSignMessage::new(); + req.address_n = path; + req.set_message(message); + let signature = handle_interaction(self.call( + req, + Box::new(|_, m: protos::EthereumMessageSignature| { + let signature = m.signature(); + + // why are you in the end + let v = signature[64] as u64; + let r = U256::from_big_endian(&signature[0..32]); + let s = U256::from_big_endian(&signature[32..64]); + + Ok(Signature { r, v, s }) + }), + )?)?; + + Ok(signature) + } + + #[allow(clippy::too_many_arguments)] + pub fn ethereum_sign_tx( + &mut self, + path: Vec, + nonce: Vec, + gas_price: Vec, + gas_limit: Vec, + to: String, + value: Vec, + _data: Vec, + chain_id: u64, + ) -> Result { + let mut req = protos::EthereumSignTx::new(); + let mut data = _data; + + req.address_n = path; + req.set_nonce(nonce); + req.set_gas_price(gas_price); + req.set_gas_limit(gas_limit); + req.set_value(value); + req.set_chain_id(chain_id); + req.set_to(to); + + req.set_data_length(data.len() as u32); + req.set_data_initial_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect()); + + let mut resp = + handle_interaction(self.call(req, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?)?; + + while resp.data_length() > 0 { + let mut ack = protos::EthereumTxAck::new(); + ack.set_data_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect()); + + resp = self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?.ok()?; + } + + if resp.signature_v() <= 1 { + resp.set_signature_v(resp.signature_v() + 2 * (chain_id as u32) + 35); + } + + Ok(Signature { + r: resp.signature_r().into(), + v: resp.signature_v().into(), + s: resp.signature_s().into(), + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn ethereum_sign_eip1559_tx( + &mut self, + path: Vec, + nonce: Vec, + gas_limit: Vec, + to: String, + value: Vec, + _data: Vec, + chain_id: u64, + max_gas_fee: Vec, + max_priority_fee: Vec, + access_list: Vec, + ) -> Result { + let mut req = protos::EthereumSignTxEIP1559::new(); + let mut data = _data; + + req.address_n = path; + req.set_nonce(nonce); + req.set_max_gas_fee(max_gas_fee); + req.set_max_priority_fee(max_priority_fee); + req.set_gas_limit(gas_limit); + req.set_value(value); + req.set_chain_id(chain_id); + req.set_to(to); + + if !access_list.is_empty() { + req.access_list = access_list + .into_iter() + .map(|item| EthereumAccessList { + address: Some(item.address), + storage_keys: item.storage_keys, + ..Default::default() + }) + .collect(); + } + + req.set_data_length(data.len() as u32); + req.set_data_initial_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect()); + + let mut resp = + handle_interaction(self.call(req, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?)?; + + while resp.data_length() > 0 { + let mut ack = protos::EthereumTxAck::new(); + ack.set_data_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect()); + + resp = self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?.ok()? + } + + if resp.signature_v() <= 1 { + resp.set_signature_v(resp.signature_v() + 2 * (chain_id as u32) + 35); + } + + Ok(Signature { + r: resp.signature_r().into(), + v: resp.signature_v().into(), + s: resp.signature_s().into(), + }) + } +} diff --git a/rust/trezor-client/src/client/mod.rs b/rust/trezor-client/src/client/mod.rs new file mode 100644 index 000000000..4666e8459 --- /dev/null +++ b/rust/trezor-client/src/client/mod.rs @@ -0,0 +1,241 @@ +#[cfg(feature = "bitcoin")] +mod bitcoin; +#[cfg(feature = "bitcoin")] +pub use self::bitcoin::*; + +#[cfg(feature = "ethereum")] +mod ethereum; +#[cfg(feature = "ethereum")] +pub use ethereum::*; + +pub mod common; +pub use common::*; + +use crate::{ + error::{Error, Result}, + messages::TrezorMessage, + protos, + protos::MessageType::*, + transport::{ProtoMessage, Transport}, + Model, +}; +use protobuf::MessageField; +use tracing::{debug, trace}; + +/// A Trezor client. +pub struct Trezor { + model: Model, + // Cached features for later inspection. + features: Option, + transport: Box, +} + +/// Create a new Trezor instance with the given transport. +pub fn trezor_with_transport(model: Model, transport: Box) -> Trezor { + Trezor { model, transport, features: None } +} + +impl Trezor { + /// Get the model of the Trezor device. + pub fn model(&self) -> Model { + self.model + } + + /// Get the features of the Trezor device. + pub fn features(&self) -> Option<&protos::Features> { + self.features.as_ref() + } + + /// Sends a message and returns the raw ProtoMessage struct that was responded by the device. + /// This method is only exported for users that want to expand the features of this library + /// f.e. for supporting additional coins etc. + pub fn call_raw(&mut self, message: S) -> Result { + let proto_msg = ProtoMessage(S::MESSAGE_TYPE, message.write_to_bytes()?); + self.transport.write_message(proto_msg).map_err(Error::TransportSendMessage)?; + self.transport.read_message().map_err(Error::TransportReceiveMessage) + } + + /// Sends a message and returns a TrezorResponse with either the expected response message, + /// a failure or an interaction request. + /// This method is only exported for users that want to expand the features of this library + /// f.e. for supporting additional coins etc. + pub fn call<'a, T, S: TrezorMessage, R: TrezorMessage>( + &'a mut self, + message: S, + result_handler: Box>, + ) -> Result> { + trace!("Sending {:?} msg: {:?}", S::MESSAGE_TYPE, message); + let resp = self.call_raw(message)?; + if resp.message_type() == R::MESSAGE_TYPE { + let resp_msg = resp.into_message()?; + trace!("Received {:?} msg: {:?}", R::MESSAGE_TYPE, resp_msg); + Ok(TrezorResponse::Ok(result_handler(self, resp_msg)?)) + } else { + match resp.message_type() { + MessageType_Failure => { + let fail_msg = resp.into_message()?; + debug!("Received failure: {:?}", fail_msg); + Ok(TrezorResponse::Failure(fail_msg)) + } + MessageType_ButtonRequest => { + let req_msg = resp.into_message()?; + trace!("Received ButtonRequest: {:?}", req_msg); + Ok(TrezorResponse::ButtonRequest(ButtonRequest { + message: req_msg, + client: self, + result_handler, + })) + } + MessageType_PinMatrixRequest => { + let req_msg = resp.into_message()?; + trace!("Received PinMatrixRequest: {:?}", req_msg); + Ok(TrezorResponse::PinMatrixRequest(PinMatrixRequest { + message: req_msg, + client: self, + result_handler, + })) + } + MessageType_PassphraseRequest => { + let req_msg = resp.into_message()?; + trace!("Received PassphraseRequest: {:?}", req_msg); + Ok(TrezorResponse::PassphraseRequest(PassphraseRequest { + message: req_msg, + client: self, + result_handler, + })) + } + mtype => { + debug!( + "Received unexpected msg type: {:?}; raw msg: {}", + mtype, + hex::encode(resp.into_payload()) + ); + Err(Error::UnexpectedMessageType(mtype)) + } + } + } + } + + pub fn init_device(&mut self, session_id: Option>) -> Result<()> { + let features = self.initialize(session_id)?.ok()?; + self.features = Some(features); + Ok(()) + } + + pub fn initialize( + &mut self, + session_id: Option>, + ) -> Result> { + let mut req = protos::Initialize::new(); + if let Some(session_id) = session_id { + req.set_session_id(session_id); + } + self.call(req, Box::new(|_, m| Ok(m))) + } + + pub fn ping(&mut self, message: &str) -> Result> { + let mut req = protos::Ping::new(); + req.set_message(message.to_owned()); + self.call(req, Box::new(|_, _| Ok(()))) + } + + pub fn change_pin(&mut self, remove: bool) -> Result> { + let mut req = protos::ChangePin::new(); + req.set_remove(remove); + self.call(req, Box::new(|_, _| Ok(()))) + } + + pub fn wipe_device(&mut self) -> Result> { + let req = protos::WipeDevice::new(); + self.call(req, Box::new(|_, _| Ok(()))) + } + + pub fn recover_device( + &mut self, + word_count: WordCount, + passphrase_protection: bool, + pin_protection: bool, + label: String, + dry_run: bool, + ) -> Result> { + let mut req = protos::RecoveryDevice::new(); + req.set_word_count(word_count as u32); + req.set_passphrase_protection(passphrase_protection); + req.set_pin_protection(pin_protection); + req.set_label(label); + req.set_enforce_wordlist(true); + req.set_dry_run(dry_run); + req.set_type( + protos::recovery_device::RecoveryDeviceType::RecoveryDeviceType_ScrambledWords, + ); + //TODO(stevenroose) support languages + req.set_language("english".to_owned()); + self.call(req, Box::new(|_, _| Ok(()))) + } + + #[allow(clippy::too_many_arguments)] + pub fn reset_device( + &mut self, + display_random: bool, + strength: usize, + passphrase_protection: bool, + pin_protection: bool, + label: String, + skip_backup: bool, + no_backup: bool, + ) -> Result, protos::EntropyRequest>> { + let mut req = protos::ResetDevice::new(); + req.set_display_random(display_random); + req.set_strength(strength as u32); + req.set_passphrase_protection(passphrase_protection); + req.set_pin_protection(pin_protection); + req.set_label(label); + req.set_skip_backup(skip_backup); + req.set_no_backup(no_backup); + self.call(req, Box::new(|c, _| Ok(EntropyRequest { client: c }))) + } + + pub fn backup(&mut self) -> Result> { + let req = protos::BackupDevice::new(); + self.call(req, Box::new(|_, _| Ok(()))) + } + + //TODO(stevenroose) support U2F stuff? currently ignored all + + pub fn apply_settings( + &mut self, + label: Option, + use_passphrase: Option, + homescreen: Option>, + auto_lock_delay_ms: Option, + ) -> Result> { + let mut req = protos::ApplySettings::new(); + if let Some(label) = label { + req.set_label(label); + } + if let Some(use_passphrase) = use_passphrase { + req.set_use_passphrase(use_passphrase); + } + if let Some(homescreen) = homescreen { + req.set_homescreen(homescreen); + } + if let Some(auto_lock_delay_ms) = auto_lock_delay_ms { + req.set_auto_lock_delay_ms(auto_lock_delay_ms as u32); + } + self.call(req, Box::new(|_, _| Ok(()))) + } + + pub fn sign_identity( + &mut self, + identity: protos::IdentityType, + digest: Vec, + curve: String, + ) -> Result, protos::SignedIdentity>> { + let mut req = protos::SignIdentity::new(); + req.identity = MessageField::some(identity); + req.set_challenge_hidden(digest); + req.set_challenge_visual("".to_owned()); + req.set_ecdsa_curve_name(curve); + self.call(req, Box::new(|_, m| Ok(m.signature().to_owned()))) + } +} diff --git a/rust/trezor-client/src/error.rs b/rust/trezor-client/src/error.rs new file mode 100644 index 000000000..5b41f19b8 --- /dev/null +++ b/rust/trezor-client/src/error.rs @@ -0,0 +1,92 @@ +//! # Error Handling + +use crate::{client::InteractionType, protos, transport::error::Error as TransportError}; + +/// Trezor result type. Aliased to [`std::result::Result`] with the error type +/// set to [`Error`]. +pub type Result = std::result::Result; + +/// Trezor error. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Less than one device was plugged in. + #[error("Trezor device not found")] + NoDeviceFound, + /// More than one device was plugged in. + #[error("multiple Trezor devices found")] + DeviceNotUnique, + /// Transport error connecting to device. + #[error("transport connect: {0}")] + TransportConnect(#[source] TransportError), + /// Transport error while beginning a session. + #[error("transport beginning session: {0}")] + TransportBeginSession(#[source] TransportError), + /// Transport error while ending a session. + #[error("transport ending session: {0}")] + TransportEndSession(#[source] TransportError), + /// Transport error while sending a message. + #[error("transport sending message: {0}")] + TransportSendMessage(#[source] TransportError), + /// Transport error while receiving a message. + #[error("transport receiving message: {0}")] + TransportReceiveMessage(#[source] TransportError), + /// Received an unexpected message type from the device. + #[error("received unexpected message type: {0:?}")] + UnexpectedMessageType(protos::MessageType), //TODO(stevenroose) type alias + /// Error reading or writing protobuf messages. + #[error(transparent)] + Protobuf(#[from] protobuf::Error), + /// A failure message was returned by the device. + #[error("failure received: code={:?} message=\"{}\"", .0.code(), .0.message())] + FailureResponse(protos::Failure), + /// An unexpected interaction request was returned by the device. + #[error("unexpected interaction request: {0:?}")] + UnexpectedInteractionRequest(InteractionType), + /// The given Bitcoin network is not supported. + #[error("given network is not supported")] + UnsupportedNetwork, + /// Provided entropy is not 32 bytes. + #[error("provided entropy is not 32 bytes")] + InvalidEntropy, + /// The device erenced a non-existing input or output index. + #[error("device referenced non-existing input or output index: {0}")] + TxRequestInvalidIndex(usize), + + /// User provided invalid PSBT. + #[error("PSBT missing input tx: {0}")] + InvalidPsbt(String), + + // bitcoin + /// Error in Base58 decoding + #[cfg(feature = "bitcoin")] + #[error(transparent)] + Base58(#[from] bitcoin::base58::Error), + /// The device erenced an unknown TXID. + #[cfg(feature = "bitcoin")] + #[error("device referenced unknown TXID: {0}")] + TxRequestUnknownTxid(bitcoin::hashes::sha256d::Hash), + /// The PSBT is missing the full tx for given input. + #[cfg(feature = "bitcoin")] + #[error("PSBT missing input tx: {0}")] + PsbtMissingInputTx(bitcoin::hashes::sha256d::Hash), + /// Device produced invalid TxRequest message. + #[cfg(feature = "bitcoin")] + #[error("malformed TxRequest: {0:?}")] + MalformedTxRequest(protos::TxRequest), + /// Error encoding/decoding a Bitcoin data structure. + #[cfg(feature = "bitcoin")] + #[error(transparent)] + BitcoinEncode(#[from] bitcoin::consensus::encode::Error), + /// Elliptic curve crypto error. + #[cfg(feature = "bitcoin")] + #[error(transparent)] + Secp256k1(#[from] bitcoin::secp256k1::Error), + /// Bip32 error. + #[cfg(feature = "bitcoin")] + #[error(transparent)] + Bip32(#[from] bitcoin::bip32::Error), + /// Address error. + #[cfg(feature = "bitcoin")] + #[error(transparent)] + Address(#[from] bitcoin::address::Error), +} diff --git a/rust/trezor-client/src/flows/sign_tx.rs b/rust/trezor-client/src/flows/sign_tx.rs new file mode 100644 index 000000000..e4b0c4a14 --- /dev/null +++ b/rust/trezor-client/src/flows/sign_tx.rs @@ -0,0 +1,334 @@ +//! Logic to handle the sign_tx command flow. + +use crate::{ + client::*, + error::{Error, Result}, + protos::{ + self, + tx_ack::{ + transaction_type::{TxOutputBinType, TxOutputType}, + TransactionType, + }, + }, + utils, +}; +use bitcoin::{hashes::sha256d, psbt, Network, Transaction}; +use protos::{ + tx_ack::transaction_type::TxInputType, tx_request::RequestType as TxRequestType, + OutputScriptType, +}; +use tracing::trace; + +// Some types with raw protos that we use in the public interface so they have to be exported. +pub use protos::{ + ButtonRequest as ButtonRequestType, Features, InputScriptType, + PinMatrixRequest as PinMatrixRequestType, +}; + +/// Fulfill a TxRequest for TXINPUT. +fn ack_input_request( + req: &protos::TxRequest, + psbt: &psbt::PartiallySignedTransaction, +) -> Result { + if req.details.is_none() || !req.details.has_request_index() { + return Err(Error::MalformedTxRequest(req.clone())) + } + + // Choose either the tx we are signing or a dependent tx. + let input_index = req.details.request_index() as usize; + let input = if req.details.has_tx_hash() { + let req_hash: sha256d::Hash = utils::from_rev_bytes(req.details.tx_hash()) + .ok_or_else(|| Error::MalformedTxRequest(req.clone()))?; + trace!("Preparing ack for input {}:{}", req_hash, input_index); + let inp = utils::psbt_find_input(psbt, req_hash)?; + let tx = inp.non_witness_utxo.as_ref().ok_or(Error::PsbtMissingInputTx(req_hash))?; + let opt = &tx.input.get(input_index); + opt.ok_or_else(|| Error::TxRequestInvalidIndex(input_index))? + } else { + trace!("Preparing ack for tx input #{}", input_index); + let opt = &psbt.unsigned_tx.input.get(input_index); + opt.ok_or(Error::TxRequestInvalidIndex(input_index))? + }; + + let mut data_input = TxInputType::new(); + data_input + .set_prev_hash(utils::to_rev_bytes(input.previous_output.txid.as_raw_hash()).to_vec()); + data_input.set_prev_index(input.previous_output.vout); + data_input.set_script_sig(input.script_sig.to_bytes()); + data_input.set_sequence(input.sequence.to_consensus_u32()); + + // Extra data only for currently signing tx. + if !req.details.has_tx_hash() { + let psbt_input = psbt + .inputs + .get(input_index) + .ok_or_else(|| Error::InvalidPsbt("not enough psbt inputs".to_owned()))?; + + // Get the output we are spending from the PSBT input. + let txout = if let Some(ref txout) = psbt_input.witness_utxo { + txout + } else if let Some(ref tx) = psbt_input.non_witness_utxo { + tx.output.get(input.previous_output.vout as usize).ok_or_else(|| { + Error::InvalidPsbt(format!("invalid utxo for PSBT input {}", input_index)) + })? + } else { + return Err(Error::InvalidPsbt(format!("no utxo for PSBT input {}", input_index))) + }; + + // If there is exactly 1 HD keypath known, we can provide it. If more it's multisig. + if psbt_input.bip32_derivation.len() == 1 { + let (_, (_, path)) = psbt_input.bip32_derivation.iter().next().unwrap(); + data_input.address_n = path.as_ref().iter().map(|i| (*i).into()).collect(); + } + + // Since we know the keypath, we probably have to sign it. So update script_type. + let script_type = { + let script_pubkey = &txout.script_pubkey; + + if script_pubkey.is_p2pkh() { + InputScriptType::SPENDADDRESS + } else if script_pubkey.is_v0_p2wpkh() || script_pubkey.is_v0_p2wsh() { + InputScriptType::SPENDWITNESS + } else if script_pubkey.is_p2sh() && psbt_input.witness_script.is_some() { + InputScriptType::SPENDP2SHWITNESS + } else { + //TODO(stevenroose) normal p2sh is probably multisig + InputScriptType::EXTERNAL + } + }; + data_input.set_script_type(script_type); + //TODO(stevenroose) multisig + + data_input.set_amount(txout.value); + } + + trace!("Prepared input to ack: {:?}", data_input); + let mut txdata = TransactionType::new(); + txdata.inputs.push(data_input); + let mut msg = protos::TxAck::new(); + msg.tx = protobuf::MessageField::some(txdata); + Ok(msg) +} + +/// Fulfill a TxRequest for TXOUTPUT. +fn ack_output_request( + req: &protos::TxRequest, + psbt: &psbt::PartiallySignedTransaction, + network: Network, +) -> Result { + if req.details.is_none() || !req.details.has_request_index() { + return Err(Error::MalformedTxRequest(req.clone())) + } + + // For outputs, the Trezor only needs bin_outputs to be set for dependent txs and full outputs + // for the signing tx. + let mut txdata = TransactionType::new(); + if req.details.has_tx_hash() { + // Dependent tx, take the output from the PSBT and just create bin_output. + let output_index = req.details.request_index() as usize; + let req_hash: sha256d::Hash = utils::from_rev_bytes(req.details.tx_hash()) + .ok_or_else(|| Error::MalformedTxRequest(req.clone()))?; + trace!("Preparing ack for output {}:{}", req_hash, output_index); + let inp = utils::psbt_find_input(psbt, req_hash)?; + let output = if let Some(ref tx) = inp.non_witness_utxo { + let opt = &tx.output.get(output_index); + opt.ok_or_else(|| Error::TxRequestInvalidIndex(output_index))? + } else if let Some(ref utxo) = inp.witness_utxo { + utxo + } else { + return Err(Error::InvalidPsbt("not all inputs have utxo data".to_owned())) + }; + + let mut bin_output = TxOutputBinType::new(); + bin_output.set_amount(output.value); + bin_output.set_script_pubkey(output.script_pubkey.to_bytes()); + + trace!("Prepared bin_output to ack: {:?}", bin_output); + txdata.bin_outputs.push(bin_output); + } else { + // Signing tx, we need to fill the full output meta object. + let output_index = req.details.request_index() as usize; + trace!("Preparing ack for tx output #{}", output_index); + let opt = &psbt.unsigned_tx.output.get(output_index); + let output = opt.ok_or(Error::TxRequestInvalidIndex(output_index))?; + + let mut data_output = TxOutputType::new(); + data_output.set_amount(output.value); + // Set script type to PAYTOADDRESS unless we find out otherwise from the PSBT. + data_output.set_script_type(OutputScriptType::PAYTOADDRESS); + if let Some(addr) = utils::address_from_script(&output.script_pubkey, network) { + data_output.set_address(addr.to_string()); + } + + let psbt_output = psbt + .outputs + .get(output_index) + .ok_or_else(|| Error::InvalidPsbt("output indices don't match".to_owned()))?; + if psbt_output.bip32_derivation.len() == 1 { + let (_, (_, path)) = psbt_output.bip32_derivation.iter().next().unwrap(); + data_output.address_n = path.as_ref().iter().map(|i| (*i).into()).collect(); + + // Since we know the keypath, it's probably a change output. So update script_type. + let script_pubkey = &psbt.unsigned_tx.output[output_index].script_pubkey; + if script_pubkey.is_op_return() { + data_output.set_script_type(OutputScriptType::PAYTOOPRETURN); + data_output.set_op_return_data(script_pubkey.as_bytes()[1..].to_vec()); + } else if psbt_output.witness_script.is_some() { + if psbt_output.redeem_script.is_some() { + data_output.set_script_type(OutputScriptType::PAYTOP2SHWITNESS); + } else { + data_output.set_script_type(OutputScriptType::PAYTOWITNESS); + } + } else { + data_output.set_script_type(OutputScriptType::PAYTOADDRESS); + } + } + + trace!("Prepared output to ack: {:?}", data_output); + txdata.outputs.push(data_output); + }; + + let mut msg = protos::TxAck::new(); + msg.tx = protobuf::MessageField::some(txdata); + Ok(msg) +} + +/// Fulfill a TxRequest for TXMETA. +fn ack_meta_request( + req: &protos::TxRequest, + psbt: &psbt::PartiallySignedTransaction, +) -> Result { + if req.details.is_none() { + return Err(Error::MalformedTxRequest(req.clone())) + } + + // Choose either the tx we are signing or a dependent tx. + let tx: &Transaction = if req.details.has_tx_hash() { + // dependeny tx, look for it in PSBT inputs + let req_hash: sha256d::Hash = utils::from_rev_bytes(req.details.tx_hash()) + .ok_or_else(|| Error::MalformedTxRequest(req.clone()))?; + trace!("Preparing ack for tx meta of {}", req_hash); + let inp = utils::psbt_find_input(psbt, req_hash)?; + inp.non_witness_utxo.as_ref().ok_or(Error::PsbtMissingInputTx(req_hash))? + } else { + // currently signing tx + trace!("Preparing ack for tx meta of tx being signed"); + &psbt.unsigned_tx + }; + + let mut txdata = TransactionType::new(); + txdata.set_version(tx.version as u32); + txdata.set_lock_time(tx.lock_time.to_consensus_u32()); + txdata.set_inputs_cnt(tx.input.len() as u32); + txdata.set_outputs_cnt(tx.output.len() as u32); + //TODO(stevenroose) python does something with extra data? + + trace!("Prepared tx meta to ack: {:?}", txdata); + let mut msg = protos::TxAck::new(); + msg.tx = protobuf::MessageField::some(txdata); + Ok(msg) +} + +/// Object to track the progress in the transaction signing flow. The device will ask for various +/// parts of the transaction and dependent transactions and can at any point also ask for user +/// interaction. The information asked for by the device is provided based on a PSBT object and the +/// resulting extra signatures are also added to the PSBT file. +/// +/// It's important to always first check with the `finished()` method if more data is requested by +/// the device. If you're not yet finished you must call the `ack_psbt()` method to send more +/// information to the device. +pub struct SignTxProgress<'a> { + client: &'a mut Trezor, + req: protos::TxRequest, +} + +impl<'a> SignTxProgress<'a> { + /// Only intended for internal usage. + pub fn new(client: &mut Trezor, req: protos::TxRequest) -> SignTxProgress<'_> { + SignTxProgress { client, req } + } + + /// Inspector to the request message received from the device. + pub fn tx_request(&self) -> &protos::TxRequest { + &self.req + } + + /// Check whether or not the signing process is finished. + pub fn finished(&self) -> bool { + self.req.request_type() == TxRequestType::TXFINISHED + } + + /// Check if a signature is provided by the device. + pub fn has_signature(&self) -> bool { + let serialized = &self.req.serialized; + serialized.is_some() && serialized.has_signature_index() && serialized.has_signature() + } + + /// Get the signature provided from the device along with the input index of the signature. + pub fn get_signature(&self) -> Option<(usize, &[u8])> { + if self.has_signature() { + let serialized = &self.req.serialized; + Some((serialized.signature_index() as usize, serialized.signature())) + } else { + None + } + } + + //TODO(stevenroose) We used to have a method here `apply_signature(&mut psbt)` that would put + // the received signature in the correct PSBT input. However, since the signature is just a raw + // signature instead of a scriptSig, this is harder. It can be done, but then we'd have to have + // the pubkey provided in the PSBT (possible thought HD path) and we'd have to do some Script + // inspection to see if we should put it as a p2pkh sciptSig or witness data. + + /// Check if a part of the serialized signed tx is provided by the device. + pub fn has_serialized_tx_part(&self) -> bool { + let serialized = &self.req.serialized; + serialized.is_some() && serialized.has_serialized_tx() + } + + /// Get the part of the serialized signed tx from the device. + pub fn get_serialized_tx_part(&self) -> Option<&[u8]> { + if self.has_serialized_tx_part() { + Some(self.req.serialized.serialized_tx()) + } else { + None + } + } + + /// Manually provide a TxAck message to the device. + /// + /// This method will panic if `finished()` returned true, + /// so it should always be checked in advance. + pub fn ack_msg( + self, + ack: protos::TxAck, + ) -> Result, protos::TxRequest>> { + assert!(!self.finished()); + + self.client.call(ack, Box::new(|c, m| Ok(SignTxProgress::new(c, m)))) + } + + /// Provide additional PSBT information to the device. + /// + /// This method will panic if `apply()` returned true, + /// so it should always be checked in advance. + pub fn ack_psbt( + self, + psbt: &psbt::PartiallySignedTransaction, + network: Network, + ) -> Result, protos::TxRequest>> { + assert!(self.req.request_type() != TxRequestType::TXFINISHED); + + let ack = match self.req.request_type() { + TxRequestType::TXINPUT => ack_input_request(&self.req, psbt), + TxRequestType::TXOUTPUT => ack_output_request(&self.req, psbt, network), + TxRequestType::TXMETA => ack_meta_request(&self.req, psbt), + TxRequestType::TXEXTRADATA => unimplemented!(), //TODO(stevenroose) implement + TxRequestType::TXORIGINPUT | + TxRequestType::TXORIGOUTPUT | + TxRequestType::TXPAYMENTREQ => unimplemented!(), + TxRequestType::TXFINISHED => unreachable!(), + }?; + self.ack_msg(ack) + } +} diff --git a/rust/trezor-client/src/lib.rs b/rust/trezor-client/src/lib.rs new file mode 100644 index 000000000..8d71fdd82 --- /dev/null +++ b/rust/trezor-client/src/lib.rs @@ -0,0 +1,187 @@ +//! # Trezor API library +//! +//! ## Connecting +//! +//! Use the public top-level methods `find_devices()` and `unique()` to find devices. When using +//! `find_devices()`, a list of different available devices is returned. To connect to one or more +//! of them, use their `connect()` method. +//! +//! ## Logging +//! +//! We use the log package interface, so any logger that supports log can be attached. +//! Please be aware that `trace` logging can contain sensitive data. + +#![warn(rust_2018_idioms)] + +mod messages; +mod transport; + +pub mod client; +pub mod error; +pub mod protos; +#[cfg(feature = "bitcoin")] +pub mod utils; + +mod flows { + #[cfg(feature = "bitcoin")] + pub mod sign_tx; +} + +pub use client::{ + ButtonRequest, ButtonRequestType, EntropyRequest, Features, PassphraseRequest, + PinMatrixRequest, PinMatrixRequestType, Trezor, TrezorResponse, WordCount, +}; +pub use error::{Error, Result}; +pub use messages::TrezorMessage; + +#[cfg(feature = "bitcoin")] +pub use flows::sign_tx::SignTxProgress; +#[cfg(feature = "bitcoin")] +pub use protos::InputScriptType; + +use std::fmt; +use tracing::{debug, warn}; +use transport::{udp::UdpTransport, webusb::WebUsbTransport}; + +/// The different kind of Trezor device models. +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +pub enum Model { + TrezorLegacy, + Trezor, + TrezorBootloader, + TrezorEmulator, +} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Model { + pub const fn as_str(&self) -> &'static str { + match self { + Model::TrezorLegacy => "Trezor (legacy)", + Model::Trezor => "Trezor", + Model::TrezorBootloader => "Trezor (bootloader)", + Model::TrezorEmulator => "Trezor Emulator", + } + } +} + +/// A device found by the `find_devices()` method. It can be connected to using the `connect()` +/// method. +#[derive(Debug)] +pub struct AvailableDevice { + pub model: Model, + pub debug: bool, + transport: transport::AvailableDeviceTransport, +} + +impl fmt::Display for AvailableDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (transport: {}) (debug: {})", self.model, &self.transport, self.debug) + } +} + +impl AvailableDevice { + /// Connect to the device. + pub fn connect(self) -> Result { + let transport = transport::connect(&self).map_err(Error::TransportConnect)?; + Ok(client::trezor_with_transport(self.model, transport)) + } +} + +/// Search for all available devices. +/// Most devices will show up twice both either debugging enables or disabled. +pub fn find_devices(debug: bool) -> Vec { + let mut devices = vec![]; + + match WebUsbTransport::find_devices(debug) { + Ok(usb) => devices.extend(usb), + Err(err) => { + warn!("{}", Error::TransportConnect(err)) + } + }; + + match UdpTransport::find_devices(debug, None) { + Ok(udp) => devices.extend(udp), + Err(err) => { + warn!("{}", Error::TransportConnect(err)) + } + }; + + devices +} + +/// Try to get a single device. Optionally specify whether debug should be enabled or not. +/// Can error if there are multiple or no devices available. +/// For more fine-grained device selection, use `find_devices()`. +/// When using USB mode, the device will show up both with debug and without debug, so it's +/// necessary to specify the debug option in order to find a unique one. +pub fn unique(debug: bool) -> Result { + let mut devices = find_devices(debug); + match devices.len() { + 0 => Err(Error::NoDeviceFound), + 1 => Ok(devices.remove(0).connect()?), + _ => { + debug!("Trezor devices found: {:?}", devices); + Err(Error::DeviceNotUnique) + } + } +} + +#[cfg(test)] +mod tests { + use serial_test::serial; + use std::str::FromStr; + + use bitcoin::bip32::DerivationPath; + + use super::*; + + fn init_emulator() -> Trezor { + let mut emulator = find_devices(false) + .into_iter() + .find(|t| t.model == Model::TrezorEmulator) + .expect("No emulator found") + .connect() + .expect("Failed to connect to emulator"); + emulator.init_device(None).expect("Failed to intialize device"); + emulator + } + + #[test] + #[serial] + fn test_emulator_find() { + let trezors = find_devices(false); + assert!(trezors.len() > 0); + assert!(trezors.iter().any(|t| t.model == Model::TrezorEmulator)); + } + + #[test] + #[serial] + fn test_emulator_features() { + let emulator = init_emulator(); + let features = emulator.features().expect("Failed to get features"); + assert_eq!(features.vendor(), "trezor.io"); + assert_eq!(features.initialized(), true); + assert_eq!(features.firmware_present(), false); + assert_eq!(features.model(), "T"); + assert_eq!(features.initialized(), true); + assert_eq!(features.pin_protection(), false); + assert_eq!(features.passphrase_protection(), false); + } + + #[test] + #[serial] + fn test_bitcoin_address() { + let mut emulator = init_emulator(); + assert_eq!(emulator.features().expect("Failed to get features").label(), "SLIP-0014"); + let path = DerivationPath::from_str("m/44'/1'/0'/0/0").expect("Failed to parse path"); + let address = emulator + .get_address(&path, InputScriptType::SPENDADDRESS, bitcoin::Network::Testnet, false) + .expect("Failed to get address"); + assert_eq!(address.ok().unwrap().to_string(), "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q"); + } +} diff --git a/rust/trezor-client/src/messages.rs b/rust/trezor-client/src/messages.rs new file mode 100644 index 000000000..4f3971f8d --- /dev/null +++ b/rust/trezor-client/src/messages.rs @@ -0,0 +1,300 @@ +//! This module implements the `message_type` getter for all protobuf message types. + +use crate::protos::{MessageType::*, *}; + +/// Extends the protobuf Message trait to also have a static getter for the message +/// type code. +pub trait TrezorMessage: protobuf::Message + std::fmt::Debug { + const MESSAGE_TYPE: MessageType; + + #[inline] + #[deprecated(note = "Use `MESSAGE_TYPE` instead")] + fn message_type() -> MessageType { + Self::MESSAGE_TYPE + } +} + +/// This macro provides the TrezorMessage trait for a protobuf message. +macro_rules! trezor_message_impl { + ($($struct:ident => $mtype:expr),+ $(,)?) => {$( + impl TrezorMessage for $struct { + const MESSAGE_TYPE: MessageType = $mtype; + } + )+}; +} + +trezor_message_impl! { + Initialize => MessageType_Initialize, + Ping => MessageType_Ping, + Success => MessageType_Success, + Failure => MessageType_Failure, + ChangePin => MessageType_ChangePin, + WipeDevice => MessageType_WipeDevice, + GetEntropy => MessageType_GetEntropy, + Entropy => MessageType_Entropy, + LoadDevice => MessageType_LoadDevice, + ResetDevice => MessageType_ResetDevice, + SetBusy => MessageType_SetBusy, + Features => MessageType_Features, + PinMatrixRequest => MessageType_PinMatrixRequest, + PinMatrixAck => MessageType_PinMatrixAck, + Cancel => MessageType_Cancel, + LockDevice => MessageType_LockDevice, + ApplySettings => MessageType_ApplySettings, + ButtonRequest => MessageType_ButtonRequest, + ButtonAck => MessageType_ButtonAck, + ApplyFlags => MessageType_ApplyFlags, + GetNonce => MessageType_GetNonce, + Nonce => MessageType_Nonce, + BackupDevice => MessageType_BackupDevice, + EntropyRequest => MessageType_EntropyRequest, + EntropyAck => MessageType_EntropyAck, + PassphraseRequest => MessageType_PassphraseRequest, + PassphraseAck => MessageType_PassphraseAck, + RecoveryDevice => MessageType_RecoveryDevice, + WordRequest => MessageType_WordRequest, + WordAck => MessageType_WordAck, + GetFeatures => MessageType_GetFeatures, + SdProtect => MessageType_SdProtect, + ChangeWipeCode => MessageType_ChangeWipeCode, + EndSession => MessageType_EndSession, + DoPreauthorized => MessageType_DoPreauthorized, + PreauthorizedRequest => MessageType_PreauthorizedRequest, + CancelAuthorization => MessageType_CancelAuthorization, + RebootToBootloader => MessageType_RebootToBootloader, + GetFirmwareHash => MessageType_GetFirmwareHash, + FirmwareHash => MessageType_FirmwareHash, + UnlockPath => MessageType_UnlockPath, + UnlockedPathRequest => MessageType_UnlockedPathRequest, + SetU2FCounter => MessageType_SetU2FCounter, + GetNextU2FCounter => MessageType_GetNextU2FCounter, + NextU2FCounter => MessageType_NextU2FCounter, + Deprecated_PassphraseStateRequest => MessageType_Deprecated_PassphraseStateRequest, + Deprecated_PassphraseStateAck => MessageType_Deprecated_PassphraseStateAck, + FirmwareErase => MessageType_FirmwareErase, + FirmwareUpload => MessageType_FirmwareUpload, + FirmwareRequest => MessageType_FirmwareRequest, + SelfTest => MessageType_SelfTest, + CipherKeyValue => MessageType_CipherKeyValue, + CipheredKeyValue => MessageType_CipheredKeyValue, + SignIdentity => MessageType_SignIdentity, + SignedIdentity => MessageType_SignedIdentity, + GetECDHSessionKey => MessageType_GetECDHSessionKey, + ECDHSessionKey => MessageType_ECDHSessionKey, + CosiCommit => MessageType_CosiCommit, + CosiCommitment => MessageType_CosiCommitment, + CosiSign => MessageType_CosiSign, + CosiSignature => MessageType_CosiSignature, + DebugLinkDecision => MessageType_DebugLinkDecision, + DebugLinkGetState => MessageType_DebugLinkGetState, + DebugLinkState => MessageType_DebugLinkState, + DebugLinkStop => MessageType_DebugLinkStop, + DebugLinkLog => MessageType_DebugLinkLog, + DebugLinkMemoryRead => MessageType_DebugLinkMemoryRead, + DebugLinkMemory => MessageType_DebugLinkMemory, + DebugLinkMemoryWrite => MessageType_DebugLinkMemoryWrite, + DebugLinkFlashErase => MessageType_DebugLinkFlashErase, + DebugLinkLayout => MessageType_DebugLinkLayout, + DebugLinkReseedRandom => MessageType_DebugLinkReseedRandom, + DebugLinkRecordScreen => MessageType_DebugLinkRecordScreen, + DebugLinkEraseSdCard => MessageType_DebugLinkEraseSdCard, + DebugLinkWatchLayout => MessageType_DebugLinkWatchLayout, + DebugLinkResetDebugEvents => MessageType_DebugLinkResetDebugEvents, +} + +#[cfg(feature = "binance")] +trezor_message_impl! { + BinanceGetAddress => MessageType_BinanceGetAddress, + BinanceAddress => MessageType_BinanceAddress, + BinanceGetPublicKey => MessageType_BinanceGetPublicKey, + BinancePublicKey => MessageType_BinancePublicKey, + BinanceSignTx => MessageType_BinanceSignTx, + BinanceTxRequest => MessageType_BinanceTxRequest, + BinanceTransferMsg => MessageType_BinanceTransferMsg, + BinanceOrderMsg => MessageType_BinanceOrderMsg, + BinanceCancelMsg => MessageType_BinanceCancelMsg, + BinanceSignedTx => MessageType_BinanceSignedTx, +} + +#[cfg(feature = "bitcoin")] +trezor_message_impl! { + GetPublicKey => MessageType_GetPublicKey, + PublicKey => MessageType_PublicKey, + SignTx => MessageType_SignTx, + TxRequest => MessageType_TxRequest, + TxAck => MessageType_TxAck, + GetAddress => MessageType_GetAddress, + Address => MessageType_Address, + TxAckPaymentRequest => MessageType_TxAckPaymentRequest, + SignMessage => MessageType_SignMessage, + VerifyMessage => MessageType_VerifyMessage, + MessageSignature => MessageType_MessageSignature, + GetOwnershipId => MessageType_GetOwnershipId, + OwnershipId => MessageType_OwnershipId, + GetOwnershipProof => MessageType_GetOwnershipProof, + OwnershipProof => MessageType_OwnershipProof, + AuthorizeCoinJoin => MessageType_AuthorizeCoinJoin, +} + +#[cfg(feature = "cardano")] +trezor_message_impl! { + CardanoGetPublicKey => MessageType_CardanoGetPublicKey, + CardanoPublicKey => MessageType_CardanoPublicKey, + CardanoGetAddress => MessageType_CardanoGetAddress, + CardanoAddress => MessageType_CardanoAddress, + CardanoTxItemAck => MessageType_CardanoTxItemAck, + CardanoTxAuxiliaryDataSupplement => MessageType_CardanoTxAuxiliaryDataSupplement, + CardanoTxWitnessRequest => MessageType_CardanoTxWitnessRequest, + CardanoTxWitnessResponse => MessageType_CardanoTxWitnessResponse, + CardanoTxHostAck => MessageType_CardanoTxHostAck, + CardanoTxBodyHash => MessageType_CardanoTxBodyHash, + CardanoSignTxFinished => MessageType_CardanoSignTxFinished, + CardanoSignTxInit => MessageType_CardanoSignTxInit, + CardanoTxInput => MessageType_CardanoTxInput, + CardanoTxOutput => MessageType_CardanoTxOutput, + CardanoAssetGroup => MessageType_CardanoAssetGroup, + CardanoToken => MessageType_CardanoToken, + CardanoTxCertificate => MessageType_CardanoTxCertificate, + CardanoTxWithdrawal => MessageType_CardanoTxWithdrawal, + CardanoTxAuxiliaryData => MessageType_CardanoTxAuxiliaryData, + CardanoPoolOwner => MessageType_CardanoPoolOwner, + CardanoPoolRelayParameters => MessageType_CardanoPoolRelayParameters, + CardanoGetNativeScriptHash => MessageType_CardanoGetNativeScriptHash, + CardanoNativeScriptHash => MessageType_CardanoNativeScriptHash, + CardanoTxMint => MessageType_CardanoTxMint, + CardanoTxCollateralInput => MessageType_CardanoTxCollateralInput, + CardanoTxRequiredSigner => MessageType_CardanoTxRequiredSigner, + CardanoTxInlineDatumChunk => MessageType_CardanoTxInlineDatumChunk, + CardanoTxReferenceScriptChunk => MessageType_CardanoTxReferenceScriptChunk, + CardanoTxReferenceInput => MessageType_CardanoTxReferenceInput, +} + +#[cfg(feature = "eos")] +trezor_message_impl! { + EosGetPublicKey => MessageType_EosGetPublicKey, + EosPublicKey => MessageType_EosPublicKey, + EosSignTx => MessageType_EosSignTx, + EosTxActionRequest => MessageType_EosTxActionRequest, + EosTxActionAck => MessageType_EosTxActionAck, + EosSignedTx => MessageType_EosSignedTx, +} + +#[cfg(feature = "ethereum")] +trezor_message_impl! { + EthereumGetPublicKey => MessageType_EthereumGetPublicKey, + EthereumPublicKey => MessageType_EthereumPublicKey, + EthereumGetAddress => MessageType_EthereumGetAddress, + EthereumAddress => MessageType_EthereumAddress, + EthereumSignTx => MessageType_EthereumSignTx, + EthereumSignTxEIP1559 => MessageType_EthereumSignTxEIP1559, + EthereumTxRequest => MessageType_EthereumTxRequest, + EthereumTxAck => MessageType_EthereumTxAck, + EthereumSignMessage => MessageType_EthereumSignMessage, + EthereumVerifyMessage => MessageType_EthereumVerifyMessage, + EthereumMessageSignature => MessageType_EthereumMessageSignature, + EthereumSignTypedData => MessageType_EthereumSignTypedData, + EthereumTypedDataStructRequest => MessageType_EthereumTypedDataStructRequest, + EthereumTypedDataStructAck => MessageType_EthereumTypedDataStructAck, + EthereumTypedDataValueRequest => MessageType_EthereumTypedDataValueRequest, + EthereumTypedDataValueAck => MessageType_EthereumTypedDataValueAck, + EthereumTypedDataSignature => MessageType_EthereumTypedDataSignature, + EthereumSignTypedHash => MessageType_EthereumSignTypedHash, +} + +#[cfg(feature = "monero")] +trezor_message_impl! { + MoneroTransactionInitRequest => MessageType_MoneroTransactionInitRequest, + MoneroTransactionInitAck => MessageType_MoneroTransactionInitAck, + MoneroTransactionSetInputRequest => MessageType_MoneroTransactionSetInputRequest, + MoneroTransactionSetInputAck => MessageType_MoneroTransactionSetInputAck, + MoneroTransactionInputViniRequest => MessageType_MoneroTransactionInputViniRequest, + MoneroTransactionInputViniAck => MessageType_MoneroTransactionInputViniAck, + MoneroTransactionAllInputsSetRequest => MessageType_MoneroTransactionAllInputsSetRequest, + MoneroTransactionAllInputsSetAck => MessageType_MoneroTransactionAllInputsSetAck, + MoneroTransactionSetOutputRequest => MessageType_MoneroTransactionSetOutputRequest, + MoneroTransactionSetOutputAck => MessageType_MoneroTransactionSetOutputAck, + MoneroTransactionAllOutSetRequest => MessageType_MoneroTransactionAllOutSetRequest, + MoneroTransactionAllOutSetAck => MessageType_MoneroTransactionAllOutSetAck, + MoneroTransactionSignInputRequest => MessageType_MoneroTransactionSignInputRequest, + MoneroTransactionSignInputAck => MessageType_MoneroTransactionSignInputAck, + MoneroTransactionFinalRequest => MessageType_MoneroTransactionFinalRequest, + MoneroTransactionFinalAck => MessageType_MoneroTransactionFinalAck, + MoneroKeyImageExportInitRequest => MessageType_MoneroKeyImageExportInitRequest, + MoneroKeyImageExportInitAck => MessageType_MoneroKeyImageExportInitAck, + MoneroKeyImageSyncStepRequest => MessageType_MoneroKeyImageSyncStepRequest, + MoneroKeyImageSyncStepAck => MessageType_MoneroKeyImageSyncStepAck, + MoneroKeyImageSyncFinalRequest => MessageType_MoneroKeyImageSyncFinalRequest, + MoneroKeyImageSyncFinalAck => MessageType_MoneroKeyImageSyncFinalAck, + MoneroGetAddress => MessageType_MoneroGetAddress, + MoneroAddress => MessageType_MoneroAddress, + MoneroGetWatchKey => MessageType_MoneroGetWatchKey, + MoneroWatchKey => MessageType_MoneroWatchKey, + DebugMoneroDiagRequest => MessageType_DebugMoneroDiagRequest, + DebugMoneroDiagAck => MessageType_DebugMoneroDiagAck, + MoneroGetTxKeyRequest => MessageType_MoneroGetTxKeyRequest, + MoneroGetTxKeyAck => MessageType_MoneroGetTxKeyAck, + MoneroLiveRefreshStartRequest => MessageType_MoneroLiveRefreshStartRequest, + MoneroLiveRefreshStartAck => MessageType_MoneroLiveRefreshStartAck, + MoneroLiveRefreshStepRequest => MessageType_MoneroLiveRefreshStepRequest, + MoneroLiveRefreshStepAck => MessageType_MoneroLiveRefreshStepAck, + MoneroLiveRefreshFinalRequest => MessageType_MoneroLiveRefreshFinalRequest, + MoneroLiveRefreshFinalAck => MessageType_MoneroLiveRefreshFinalAck, +} + +#[cfg(feature = "nem")] +trezor_message_impl! { + NEMGetAddress => MessageType_NEMGetAddress, + NEMAddress => MessageType_NEMAddress, + NEMSignTx => MessageType_NEMSignTx, + NEMSignedTx => MessageType_NEMSignedTx, + NEMDecryptMessage => MessageType_NEMDecryptMessage, + NEMDecryptedMessage => MessageType_NEMDecryptedMessage, +} + +#[cfg(feature = "ripple")] +trezor_message_impl! { + RippleGetAddress => MessageType_RippleGetAddress, + RippleAddress => MessageType_RippleAddress, + RippleSignTx => MessageType_RippleSignTx, + RippleSignedTx => MessageType_RippleSignedTx, +} + +#[cfg(feature = "stellar")] +trezor_message_impl! { + StellarSignTx => MessageType_StellarSignTx, + StellarTxOpRequest => MessageType_StellarTxOpRequest, + StellarGetAddress => MessageType_StellarGetAddress, + StellarAddress => MessageType_StellarAddress, + StellarCreateAccountOp => MessageType_StellarCreateAccountOp, + StellarPaymentOp => MessageType_StellarPaymentOp, + StellarPathPaymentStrictReceiveOp => MessageType_StellarPathPaymentStrictReceiveOp, + StellarManageSellOfferOp => MessageType_StellarManageSellOfferOp, + StellarCreatePassiveSellOfferOp => MessageType_StellarCreatePassiveSellOfferOp, + StellarSetOptionsOp => MessageType_StellarSetOptionsOp, + StellarChangeTrustOp => MessageType_StellarChangeTrustOp, + StellarAllowTrustOp => MessageType_StellarAllowTrustOp, + StellarAccountMergeOp => MessageType_StellarAccountMergeOp, + StellarManageDataOp => MessageType_StellarManageDataOp, + StellarBumpSequenceOp => MessageType_StellarBumpSequenceOp, + StellarManageBuyOfferOp => MessageType_StellarManageBuyOfferOp, + StellarPathPaymentStrictSendOp => MessageType_StellarPathPaymentStrictSendOp, + StellarSignedTx => MessageType_StellarSignedTx, +} + +#[cfg(feature = "tezos")] +trezor_message_impl! { + TezosGetAddress => MessageType_TezosGetAddress, + TezosAddress => MessageType_TezosAddress, + TezosSignTx => MessageType_TezosSignTx, + TezosSignedTx => MessageType_TezosSignedTx, + TezosGetPublicKey => MessageType_TezosGetPublicKey, + TezosPublicKey => MessageType_TezosPublicKey, +} + +#[cfg(feature = "webauthn")] +trezor_message_impl! { + WebAuthnListResidentCredentials => MessageType_WebAuthnListResidentCredentials, + WebAuthnCredentials => MessageType_WebAuthnCredentials, + WebAuthnAddResidentCredential => MessageType_WebAuthnAddResidentCredential, + WebAuthnRemoveResidentCredential => MessageType_WebAuthnRemoveResidentCredential, +} diff --git a/rust/trezor-client/src/transport/error.rs b/rust/trezor-client/src/transport/error.rs new file mode 100644 index 000000000..282ae16f6 --- /dev/null +++ b/rust/trezor-client/src/transport/error.rs @@ -0,0 +1,49 @@ +//! # Error Handling + +/// Trezor error. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// [rusb] error. + #[error(transparent)] + Usb(#[from] rusb::Error), + + /// [std::io] error. + #[error(transparent)] + IO(#[from] std::io::Error), + + /// The device to connect to was not found. + #[error("the device to connect to was not found")] + DeviceNotFound, + + /// The device is no longer available. + #[error("the device is no longer available")] + DeviceDisconnected, + + /// The device produced a data chunk of unexpected size. + #[error("the device produced a data chunk of unexpected size")] + UnexpectedChunkSizeFromDevice(usize), + + /// Timeout expired while reading from device. + #[error("timeout expired while reading from device")] + DeviceReadTimeout, + + /// The device sent a chunk with a wrong magic value. + #[error("the device sent a chunk with a wrong magic value")] + DeviceBadMagic, + + /// The device sent a message with a wrong session id. + #[error("the device sent a message with a wrong session id")] + DeviceBadSessionId, + + /// The device sent an unexpected sequence number. + #[error("the device sent an unexpected sequence number")] + DeviceUnexpectedSequenceNumber, + + /// Received a non-existing message type from the device. + #[error("received a non-existing message type from the device")] + InvalidMessageType(u32), + + /// Unable to determine device serial number. + #[error("unable to determine device serial number")] + NoDeviceSerial, +} diff --git a/rust/trezor-client/src/transport/mod.rs b/rust/trezor-client/src/transport/mod.rs new file mode 100644 index 000000000..2b963eeba --- /dev/null +++ b/rust/trezor-client/src/transport/mod.rs @@ -0,0 +1,85 @@ +use super::{AvailableDevice, Model}; +use crate::protos::MessageType; +use std::fmt; + +pub mod error; +pub mod protocol; +pub mod udp; +pub mod webusb; + +/// An available transport for a Trezor device, containing any of the different supported +/// transports. +#[derive(Debug)] +pub enum AvailableDeviceTransport { + WebUsb(webusb::AvailableWebUsbTransport), + Udp(udp::AvailableUdpTransport), +} + +impl fmt::Display for AvailableDeviceTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AvailableDeviceTransport::WebUsb(ref t) => write!(f, "{}", t), + AvailableDeviceTransport::Udp(ref t) => write!(f, "{}", t), + } + } +} + +/// A protobuf message accompanied by the message type. This type is used to pass messages over the +/// transport and used to contain messages received from the transport. +pub struct ProtoMessage(pub MessageType, pub Vec); + +impl ProtoMessage { + pub fn new(mt: MessageType, payload: Vec) -> ProtoMessage { + ProtoMessage(mt, payload) + } + pub fn message_type(&self) -> MessageType { + self.0 + } + pub fn payload(&self) -> &[u8] { + &self.1 + } + pub fn into_payload(self) -> Vec { + self.1 + } + + /// Take the payload from the ProtoMessage and parse it to a protobuf message. + pub fn into_message(self) -> Result { + protobuf::Message::parse_from_bytes(&self.into_payload()) + } +} + +/// The transport interface that is implemented by the different ways to communicate with a Trezor +/// device. +pub trait Transport { + fn session_begin(&mut self) -> Result<(), error::Error>; + fn session_end(&mut self) -> Result<(), error::Error>; + + fn write_message(&mut self, message: ProtoMessage) -> Result<(), error::Error>; + fn read_message(&mut self) -> Result; +} + +/// A delegation method to connect an available device transport. It delegates to the different +/// transport types. +pub fn connect(available_device: &AvailableDevice) -> Result, error::Error> { + match available_device.transport { + AvailableDeviceTransport::WebUsb(_) => webusb::WebUsbTransport::connect(available_device), + AvailableDeviceTransport::Udp(_) => udp::UdpTransport::connect(available_device), + } +} + +// A collection of transport-global constants. +mod constants { + pub const DEV_TREZOR_LEGACY: (u16, u16) = (0x534C, 0x0001); + pub const DEV_TREZOR: (u16, u16) = (0x1209, 0x53C1); + pub const DEV_TREZOR_BOOTLOADER: (u16, u16) = (0x1209, 0x53C0); +} + +/// Derive the Trezor model from the USB device. +pub(crate) fn derive_model(dev_id: (u16, u16)) -> Option { + match dev_id { + constants::DEV_TREZOR_LEGACY => Some(Model::TrezorLegacy), + constants::DEV_TREZOR => Some(Model::Trezor), + constants::DEV_TREZOR_BOOTLOADER => Some(Model::TrezorBootloader), + _ => None, + } +} diff --git a/rust/trezor-client/src/transport/protocol.rs b/rust/trezor-client/src/transport/protocol.rs new file mode 100644 index 000000000..e058b5a9b --- /dev/null +++ b/rust/trezor-client/src/transport/protocol.rs @@ -0,0 +1,208 @@ +use crate::{ + protos::MessageType, + transport::{error::Error, ProtoMessage}, +}; +use byteorder::{BigEndian, ByteOrder}; +use protobuf::Enum; +use std::cmp; + +/// A link represents a serial connection to send and receive byte chunks from and to a device. +pub trait Link { + fn write_chunk(&mut self, chunk: Vec) -> Result<(), Error>; + fn read_chunk(&mut self) -> Result, Error>; +} + +/// A protocol is used to encode messages in chunks that can be sent to the device and to parse +/// chunks into messages. +pub trait Protocol { + fn session_begin(&mut self) -> Result<(), Error>; + fn session_end(&mut self) -> Result<(), Error>; + fn write(&mut self, message: ProtoMessage) -> Result<(), Error>; + fn read(&mut self) -> Result; +} + +/// The length of the chunks sent. +const REPLEN: usize = 64; + +/// V2 of the binary protocol. +/// This version is currently not in use by any device and is subject to change. +#[allow(dead_code)] +pub struct ProtocolV2 { + pub link: L, + pub session_id: u32, +} + +impl Protocol for ProtocolV2 { + fn session_begin(&mut self) -> Result<(), Error> { + let mut chunk = vec![0; REPLEN]; + chunk[0] = 0x03; + self.link.write_chunk(chunk)?; + let resp = self.link.read_chunk()?; + if resp[0] != 0x03 { + println!("bad magic in v2 session_begin: {:x} instead of 0x03", resp[0]); + return Err(Error::DeviceBadMagic) + } + self.session_id = BigEndian::read_u32(&resp[1..5]); + Ok(()) + } + + fn session_end(&mut self) -> Result<(), Error> { + assert!(self.session_id != 0); + let mut chunk = vec![0; REPLEN]; + chunk[0] = 0x04; + BigEndian::write_u32(&mut chunk[1..5], self.session_id); + self.link.write_chunk(chunk)?; + let resp = self.link.read_chunk()?; + if resp[0] != 0x04 { + println!("bad magic in v2 session_end: {:x} instead of 0x04", resp[0]); + return Err(Error::DeviceBadMagic) + } + self.session_id = 0; + Ok(()) + } + + fn write(&mut self, message: ProtoMessage) -> Result<(), Error> { + assert!(self.session_id != 0); + + // First generate the total payload, then write it to the transport in chunks. + let mut data = vec![0; 8]; + BigEndian::write_u32(&mut data[0..4], message.message_type() as u32); + BigEndian::write_u32(&mut data[4..8], message.payload().len() as u32); + data.extend(message.into_payload()); + + let mut cur: usize = 0; + let mut seq: isize = -1; + while cur < data.len() { + // Build header. + let mut chunk = if seq < 0 { + let mut header = vec![0; 5]; + header[0] = 0x01; + BigEndian::write_u32(&mut header[1..5], self.session_id); + header + } else { + let mut header = vec![0; 9]; + header[0] = 0x01; + BigEndian::write_u32(&mut header[1..5], self.session_id); + BigEndian::write_u32(&mut header[5..9], seq as u32); + header + }; + seq += 1; + + // Fill remainder. + let end = cmp::min(cur + (REPLEN - chunk.len()), data.len()); + chunk.extend(&data[cur..end]); + cur = end; + debug_assert!(chunk.len() <= REPLEN); + chunk.resize(REPLEN, 0); + + self.link.write_chunk(chunk)?; + } + + Ok(()) + } + + fn read(&mut self) -> Result { + debug_assert!(self.session_id != 0); + + let chunk = self.link.read_chunk()?; + if chunk[0] != 0x01 { + println!("bad magic in v2 read: {:x} instead of 0x01", chunk[0]); + return Err(Error::DeviceBadMagic) + } + if BigEndian::read_u32(&chunk[1..5]) != self.session_id { + return Err(Error::DeviceBadSessionId) + } + let message_type_id = BigEndian::read_u32(&chunk[5..9]); + let message_type = MessageType::from_i32(message_type_id as i32) + .ok_or(Error::InvalidMessageType(message_type_id))?; + let data_length = BigEndian::read_u32(&chunk[9..13]) as usize; + + let mut data: Vec = chunk[13..].into(); + let mut seq = 0; + while data.len() < data_length { + let chunk = self.link.read_chunk()?; + if chunk[0] != 0x02 { + println!("bad magic in v2 session_begin: {:x} instead of 0x02", chunk[0]); + return Err(Error::DeviceBadMagic) + } + if BigEndian::read_u32(&chunk[1..5]) != self.session_id { + return Err(Error::DeviceBadSessionId) + } + if BigEndian::read_u32(&chunk[5..9]) != seq as u32 { + return Err(Error::DeviceUnexpectedSequenceNumber) + } + seq += 1; + + data.extend(&chunk[9..]); + } + + Ok(ProtoMessage(message_type, data[0..data_length].into())) + } +} + +/// The original binary protocol. +pub struct ProtocolV1 { + pub link: L, +} + +impl Protocol for ProtocolV1 { + fn session_begin(&mut self) -> Result<(), Error> { + Ok(()) // no sessions + } + + fn session_end(&mut self) -> Result<(), Error> { + Ok(()) // no sessions + } + + fn write(&mut self, message: ProtoMessage) -> Result<(), Error> { + // First generate the total payload, then write it to the transport in chunks. + let mut data = vec![0; 8]; + data[0] = 0x23; + data[1] = 0x23; + BigEndian::write_u16(&mut data[2..4], message.message_type() as u16); + BigEndian::write_u32(&mut data[4..8], message.payload().len() as u32); + data.extend(message.into_payload()); + + let mut cur: usize = 0; + while cur < data.len() { + let mut chunk = vec![0x3f]; + let end = cmp::min(cur + (REPLEN - 1), data.len()); + chunk.extend(&data[cur..end]); + cur = end; + debug_assert!(chunk.len() <= REPLEN); + chunk.resize(REPLEN, 0); + + self.link.write_chunk(chunk)?; + } + + Ok(()) + } + + fn read(&mut self) -> Result { + let chunk = self.link.read_chunk()?; + if chunk[0] != 0x3f || chunk[1] != 0x23 || chunk[2] != 0x23 { + println!( + "bad magic in v1 read: {:x}{:x}{:x} instead of 0x3f2323", + chunk[0], chunk[1], chunk[2] + ); + return Err(Error::DeviceBadMagic) + } + let message_type_id = BigEndian::read_u16(&chunk[3..5]) as u32; + let message_type = MessageType::from_i32(message_type_id as i32) + .ok_or(Error::InvalidMessageType(message_type_id))?; + let data_length = BigEndian::read_u32(&chunk[5..9]) as usize; + let mut data: Vec = chunk[9..].into(); + + while data.len() < data_length { + let chunk = self.link.read_chunk()?; + if chunk[0] != 0x3f { + println!("bad magic in v1 read: {:x} instead of 0x3f", chunk[0]); + return Err(Error::DeviceBadMagic) + } + + data.extend(&chunk[1..]); + } + + Ok(ProtoMessage(message_type, data[0..data_length].into())) + } +} diff --git a/rust/trezor-client/src/transport/udp.rs b/rust/trezor-client/src/transport/udp.rs new file mode 100644 index 000000000..cc83195ee --- /dev/null +++ b/rust/trezor-client/src/transport/udp.rs @@ -0,0 +1,153 @@ +use super::{ + error::Error, + protocol::{Link, Protocol, ProtocolV1}, + AvailableDeviceTransport, ProtoMessage, Transport, +}; +use crate::{AvailableDevice, Model}; +use std::{fmt, net::UdpSocket, result::Result, time::Duration}; + +// A collection of constants related to the Emulator Ports. +mod constants { + pub const DEFAULT_HOST: &str = "127.0.0.1"; + pub const DEFAULT_PORT: &str = "21324"; + pub const DEFAULT_DEBUG_PORT: &str = "21325"; + pub const LOCAL_LISTENER: &str = "127.0.0.1:0"; +} + +use constants::{DEFAULT_DEBUG_PORT, DEFAULT_HOST, DEFAULT_PORT, LOCAL_LISTENER}; + +/// The chunk size for the serial protocol. +const CHUNK_SIZE: usize = 64; + +const READ_TIMEOUT_MS: u64 = 100000; +const WRITE_TIMEOUT_MS: u64 = 100000; + +/// An available transport for connecting with a device. +#[derive(Debug)] +pub struct AvailableUdpTransport { + pub host: String, + pub port: String, +} + +impl fmt::Display for AvailableUdpTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "udp:{}:{}", self.host, self.port) + } +} + +/// An actual serial HID USB link to a device over which bytes can be sent. +struct UdpLink { + pub socket: UdpSocket, + pub device: (String, String), +} +// No need to implement drop as every member is owned + +impl Link for UdpLink { + fn write_chunk(&mut self, chunk: Vec) -> Result<(), Error> { + debug_assert_eq!(CHUNK_SIZE, chunk.len()); + let timeout = Duration::from_millis(WRITE_TIMEOUT_MS); + self.socket.set_write_timeout(Some(timeout))?; + if let Err(e) = self.socket.send(&chunk) { + return Err(e.into()) + } + Ok(()) + } + + fn read_chunk(&mut self) -> Result, Error> { + let mut chunk = vec![0; CHUNK_SIZE]; + let timeout = Duration::from_millis(READ_TIMEOUT_MS); + self.socket.set_read_timeout(Some(timeout))?; + + let n = self.socket.recv(&mut chunk)?; + if n == CHUNK_SIZE { + Ok(chunk) + } else { + Err(Error::DeviceReadTimeout) + } + } +} + +impl UdpLink { + pub fn open(path: &str) -> Result { + let mut parts = path.split(':'); + let link = Self { + socket: UdpSocket::bind(LOCAL_LISTENER)?, + device: ( + parts.next().expect("Incorrect Path").to_owned(), + parts.next().expect("Incorrect Path").to_owned(), + ), + }; + link.socket.connect(path)?; + Ok(link) + } + + // Ping the port and compare against expected response + fn ping(&self) -> Result { + let mut resp = [0; CHUNK_SIZE]; + self.socket.send("PINGPING".as_bytes())?; + let size = self.socket.recv(&mut resp)?; + Ok(&resp[..size] == "PONGPONG".as_bytes()) + } +} + +/// An implementation of the Transport interface for UDP devices. +pub struct UdpTransport { + protocol: ProtocolV1, +} + +impl UdpTransport { + pub fn find_devices(debug: bool, path: Option<&str>) -> Result, Error> { + let mut devices = Vec::new(); + let mut dest = String::new(); + match path { + Some(p) => dest = p.to_owned(), + None => { + dest.push_str(DEFAULT_HOST); + dest.push(':'); + dest.push_str(if debug { DEFAULT_DEBUG_PORT } else { DEFAULT_PORT }); + } + }; + let link = UdpLink::open(&dest)?; + if link.ping()? { + devices.push(AvailableDevice { + model: Model::TrezorEmulator, + debug, + transport: AvailableDeviceTransport::Udp(AvailableUdpTransport { + host: link.device.0, + port: link.device.1, + }), + }); + } + Ok(devices) + } + + /// Connect to a device over the UDP transport. + pub fn connect(device: &AvailableDevice) -> Result, Error> { + let transport = match device.transport { + AvailableDeviceTransport::Udp(ref t) => t, + _ => panic!("passed wrong AvailableDevice in UdpTransport::connect"), + }; + let mut path = String::new(); + path.push_str(&transport.host); + path.push(':'); + path.push_str(&transport.port); + let link = UdpLink::open(&path)?; + Ok(Box::new(UdpTransport { protocol: ProtocolV1 { link } })) + } +} + +impl super::Transport for UdpTransport { + fn session_begin(&mut self) -> Result<(), Error> { + self.protocol.session_begin() + } + fn session_end(&mut self) -> Result<(), Error> { + self.protocol.session_end() + } + + fn write_message(&mut self, message: ProtoMessage) -> Result<(), Error> { + self.protocol.write(message) + } + fn read_message(&mut self) -> Result { + self.protocol.read() + } +} diff --git a/rust/trezor-client/src/transport/webusb.rs b/rust/trezor-client/src/transport/webusb.rs new file mode 100644 index 000000000..3e3274706 --- /dev/null +++ b/rust/trezor-client/src/transport/webusb.rs @@ -0,0 +1,175 @@ +use crate::{ + transport::{ + derive_model, + error::Error, + protocol::{Link, Protocol, ProtocolV1}, + AvailableDeviceTransport, ProtoMessage, Transport, + }, + AvailableDevice, +}; +use rusb::*; +use std::{fmt, result::Result, time::Duration}; + +// A collection of constants related to the WebUsb protocol. +mod constants { + pub use crate::transport::constants::*; + + pub const CONFIG_ID: u8 = 0; + pub const INTERFACE_DESCRIPTOR: u8 = 0; + pub const LIBUSB_CLASS_VENDOR_SPEC: u8 = 0xff; + + pub const INTERFACE: u8 = 0; + pub const INTERFACE_DEBUG: u8 = 1; + pub const ENDPOINT: u8 = 1; + pub const ENDPOINT_DEBUG: u8 = 2; + pub const READ_ENDPOINT_MASK: u8 = 0x80; +} + +/// The chunk size for the serial protocol. +const CHUNK_SIZE: usize = 64; + +const READ_TIMEOUT_MS: u64 = 100000; +const WRITE_TIMEOUT_MS: u64 = 100000; + +/// An available transport for connecting with a device. +#[derive(Debug)] +pub struct AvailableWebUsbTransport { + pub bus: u8, + pub address: u8, +} + +impl fmt::Display for AvailableWebUsbTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "WebUSB ({}:{})", self.bus, self.address) + } +} + +/// An actual serial USB link to a device over which bytes can be sent. +pub struct WebUsbLink { + handle: DeviceHandle, + endpoint: u8, +} + +impl Link for WebUsbLink { + fn write_chunk(&mut self, chunk: Vec) -> Result<(), Error> { + debug_assert_eq!(CHUNK_SIZE, chunk.len()); + let timeout = Duration::from_millis(WRITE_TIMEOUT_MS); + if let Err(e) = self.handle.write_interrupt(self.endpoint, &chunk, timeout) { + return Err(e.into()) + } + Ok(()) + } + + fn read_chunk(&mut self) -> Result, Error> { + let mut chunk = vec![0; CHUNK_SIZE]; + let endpoint = constants::READ_ENDPOINT_MASK | self.endpoint; + let timeout = Duration::from_millis(READ_TIMEOUT_MS); + + let n = self.handle.read_interrupt(endpoint, &mut chunk, timeout)?; + if n == CHUNK_SIZE { + Ok(chunk) + } else { + Err(Error::DeviceReadTimeout) + } + } +} + +/// An implementation of the Transport interface for WebUSB devices. +pub struct WebUsbTransport { + protocol: ProtocolV1, +} + +impl WebUsbTransport { + pub fn find_devices(debug: bool) -> Result, Error> { + let mut devices = Vec::new(); + + for dev in rusb::devices().unwrap().iter() { + let desc = dev.device_descriptor()?; + let dev_id = (desc.vendor_id(), desc.product_id()); + + let model = match derive_model(dev_id) { + Some(m) => m, + None => continue, + }; + + // Check something with interface class code like python-trezor does. + let class_code = dev + .config_descriptor(constants::CONFIG_ID)? + .interfaces() + .find(|i| i.number() == constants::INTERFACE) + .ok_or(rusb::Error::Other)? + .descriptors() + .find(|d| d.setting_number() == constants::INTERFACE_DESCRIPTOR) + .ok_or(rusb::Error::Other)? + .class_code(); + if class_code != constants::LIBUSB_CLASS_VENDOR_SPEC { + continue + } + + devices.push(AvailableDevice { + model, + debug, + transport: AvailableDeviceTransport::WebUsb(AvailableWebUsbTransport { + bus: dev.bus_number(), + address: dev.address(), + }), + }); + } + Ok(devices) + } + + /// Connect to a device over the WebUSB transport. + pub fn connect(device: &AvailableDevice) -> Result, Error> { + let transport = match &device.transport { + AvailableDeviceTransport::WebUsb(t) => t, + _ => panic!("passed wrong AvailableDevice in WebUsbTransport::connect"), + }; + + let interface = match device.debug { + false => constants::INTERFACE, + true => constants::INTERFACE_DEBUG, + }; + + // Go over the devices again to match the desired device. + let dev = rusb::devices()? + .iter() + .find(|dev| dev.bus_number() == transport.bus && dev.address() == transport.address) + .ok_or(Error::DeviceDisconnected)?; + // Check if there is not another device connected on this bus. + let dev_desc = dev.device_descriptor()?; + let dev_id = (dev_desc.vendor_id(), dev_desc.product_id()); + if derive_model(dev_id).as_ref() != Some(&device.model) { + return Err(Error::DeviceDisconnected) + } + let mut handle = dev.open()?; + handle.claim_interface(interface)?; + + Ok(Box::new(WebUsbTransport { + protocol: ProtocolV1 { + link: WebUsbLink { + handle, + endpoint: match device.debug { + false => constants::ENDPOINT, + true => constants::ENDPOINT_DEBUG, + }, + }, + }, + })) + } +} + +impl super::Transport for WebUsbTransport { + fn session_begin(&mut self) -> Result<(), Error> { + self.protocol.session_begin() + } + fn session_end(&mut self) -> Result<(), Error> { + self.protocol.session_end() + } + + fn write_message(&mut self, message: ProtoMessage) -> Result<(), Error> { + self.protocol.write(message) + } + fn read_message(&mut self) -> Result { + self.protocol.read() + } +} diff --git a/rust/trezor-client/src/utils.rs b/rust/trezor-client/src/utils.rs new file mode 100644 index 000000000..12e6236b1 --- /dev/null +++ b/rust/trezor-client/src/utils.rs @@ -0,0 +1,76 @@ +use crate::error::{Error, Result}; +use bitcoin::{ + address, + address::Payload, + bip32, + blockdata::script::Script, + hashes::{sha256d, Hash}, + psbt, + secp256k1::ecdsa::{RecoverableSignature, RecoveryId}, + Address, Network, +}; + +/// Retrieve an address from the given script. +pub fn address_from_script(script: &Script, network: Network) -> Option { + let payload = Payload::from_script(script).ok()?; + Some(Address::new(network, payload)) +} + +/// Find the (first if multiple) PSBT input that refers to the given txid. +pub fn psbt_find_input( + psbt: &psbt::PartiallySignedTransaction, + txid: sha256d::Hash, +) -> Result<&psbt::Input> { + let inputs = &psbt.unsigned_tx.input; + let idx = inputs + .iter() + .position(|tx| *tx.previous_output.txid.as_raw_hash() == txid) + .ok_or(Error::TxRequestUnknownTxid(txid))?; + psbt.inputs.get(idx).ok_or(Error::TxRequestInvalidIndex(idx)) +} + +/// Get a hash from a reverse byte representation. +pub fn from_rev_bytes(rev_bytes: &[u8]) -> Option { + let mut bytes = rev_bytes.to_vec(); + bytes.reverse(); + sha256d::Hash::from_slice(&bytes).ok() +} + +/// Get the reverse byte representation of a hash. +pub fn to_rev_bytes(hash: &sha256d::Hash) -> [u8; 32] { + let mut bytes = hash.to_byte_array(); + bytes.reverse(); + bytes +} + +/// Parse a Bitcoin Core-style 65-byte recoverable signature. +pub fn parse_recoverable_signature( + sig: &[u8], +) -> Result { + if sig.len() != 65 { + return Err(bitcoin::secp256k1::Error::InvalidSignature) + } + + // Bitcoin Core sets the first byte to `27 + rec + (fCompressed ? 4 : 0)`. + let rec_id = RecoveryId::from_i32(if sig[0] >= 31 { + (sig[0] - 31) as i32 + } else { + (sig[0] - 27) as i32 + })?; + + RecoverableSignature::from_compact(&sig[1..], rec_id) +} + +/// Convert a bitcoin network constant to the Trezor-compatible coin_name string. +pub fn coin_name(network: Network) -> Result { + match network { + Network::Bitcoin => Ok("Bitcoin".to_owned()), + Network::Testnet => Ok("Testnet".to_owned()), + _ => Err(Error::UnsupportedNetwork), + } +} + +/// Convert a BIP-32 derivation path into a `Vec`. +pub fn convert_path(path: &bip32::DerivationPath) -> Vec { + path.into_iter().map(|i| u32::from(*i)).collect() +}