mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-22 22:38:08 +00:00
feat(rust): add trezor-client crate
This commit is contained in:
parent
20c9d81018
commit
07ba960ab4
1
rust/trezor-client/.clippy.toml
Normal file
1
rust/trezor-client/.clippy.toml
Normal file
@ -0,0 +1 @@
|
||||
msrv = "1.60"
|
1
rust/trezor-client/.gitignore
vendored
Normal file
1
rust/trezor-client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target/
|
885
rust/trezor-client/Cargo.lock
generated
Normal file
885
rust/trezor-client/Cargo.lock
generated
Normal file
@ -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"
|
57
rust/trezor-client/Cargo.toml
Normal file
57
rust/trezor-client/Cargo.toml
Normal file
@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "trezor-client"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
"joshie",
|
||||
"DaniPopes <57450786+DaniPopes@users.noreply.github.com>",
|
||||
"romanz",
|
||||
"Steven Roose <steven@stevenroose.org>",
|
||||
]
|
||||
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 = []
|
122
rust/trezor-client/LICENSE
Normal file
122
rust/trezor-client/LICENSE
Normal file
@ -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.
|
||||
|
43
rust/trezor-client/README.md
Normal file
43
rust/trezor-client/README.md
Normal file
@ -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
|
31
rust/trezor-client/examples/change_pin.rs
Normal file
31
rust/trezor-client/examples/change_pin.rs
Normal file
@ -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()
|
||||
}
|
98
rust/trezor-client/examples/features.rs
Normal file
98
rust/trezor-client/examples/features.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use std::io;
|
||||
|
||||
fn convert_path_from_str(derivation: &str) -> Vec<u32> {
|
||||
let derivation = derivation.to_string();
|
||||
let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
|
||||
|
||||
let mut path = vec![];
|
||||
for derivation_index in elements {
|
||||
let hardened = derivation_index.contains('\'');
|
||||
let mut index = derivation_index.replace('\'', "").parse::<u32>().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()
|
||||
}
|
11
rust/trezor-client/examples/find.rs
Normal file
11
rust/trezor-client/examples/find.rs
Normal file
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
57
rust/trezor-client/examples/interaction.rs
Normal file
57
rust/trezor-client/examples/interaction.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::io;
|
||||
|
||||
use bitcoin::{bip32, network::constants::Network, Address};
|
||||
use trezor_client::{Error, TrezorMessage, TrezorResponse};
|
||||
|
||||
fn handle_interaction<T, R: TrezorMessage>(resp: TrezorResponse<T, R>) -> Result<T, Error> {
|
||||
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()
|
||||
}
|
73
rust/trezor-client/examples/sign_message.rs
Normal file
73
rust/trezor-client/examples/sign_message.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::io;
|
||||
|
||||
use bitcoin::{bip32, network::constants::Network, Address};
|
||||
use trezor_client::{InputScriptType, TrezorMessage, TrezorResponse};
|
||||
|
||||
fn handle_interaction<T, R: TrezorMessage>(resp: TrezorResponse<T, R>) -> 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);
|
||||
}
|
119
rust/trezor-client/examples/sign_tx.rs
Normal file
119
rust/trezor-client/examples/sign_tx.rs
Normal file
@ -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<T, R: TrezorMessage>(resp: TrezorResponse<T, R>) -> 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<u8>,
|
||||
) -> 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));
|
||||
}
|
11
rust/trezor-client/rustfmt.toml
Normal file
11
rust/trezor-client/rustfmt.toml
Normal file
@ -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_*"]
|
108
rust/trezor-client/scripts/generate-messages.py
Executable file
108
rust/trezor-client/scripts/generate-messages.py
Executable file
@ -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()
|
16
rust/trezor-client/scripts/generate-protos.sh
Executable file
16
rust/trezor-client/scripts/generate-protos.sh
Executable file
@ -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
|
95
rust/trezor-client/src/client/bitcoin.rs
Normal file
95
rust/trezor-client/src/client/bitcoin.rs
Normal file
@ -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<TrezorResponse<'_, bip32::ExtendedPubKey, protos::PublicKey>> {
|
||||
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<TrezorResponse<'_, Address, protos::Address>> {
|
||||
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<TrezorResponse<'_, SignTxProgress<'_>, 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<TrezorResponse<'_, (Address, RecoverableSignature), protos::MessageSignature>> {
|
||||
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<Address> {
|
||||
let address = s.parse::<Address<NetworkUnchecked>>()?;
|
||||
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::<String>(),
|
||||
}
|
||||
}
|
247
rust/trezor-client/src/client/common.rs
Normal file
247
rust/trezor-client/src/client/common.rs
Normal file
@ -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<T>;
|
||||
|
||||
/// 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<ResultHandler<'a, T, R>>,
|
||||
}
|
||||
|
||||
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<TrezorResponse<'a, T, R>> {
|
||||
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<ResultHandler<'a, T, R>>,
|
||||
}
|
||||
|
||||
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<TrezorResponse<'a, T, R>> {
|
||||
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<ResultHandler<'a, T, R>>,
|
||||
}
|
||||
|
||||
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<TrezorResponse<'a, T, R>> {
|
||||
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<TrezorResponse<'a, T, R>> {
|
||||
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<FnOnce> 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<T> {
|
||||
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<ButtonRequest<'a, T, R>> {
|
||||
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<PinMatrixRequest<'a, T, R>> {
|
||||
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<PassphraseRequest<'a, T, R>> {
|
||||
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<T, R: TrezorMessage>(resp: TrezorResponse<'_, T, R>) -> Result<T> {
|
||||
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<u8>) -> Result<TrezorResponse<'a, (), protos::Success>> {
|
||||
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(())))
|
||||
}
|
||||
}
|
167
rust/trezor-client/src/client/ethereum.rs
Normal file
167
rust/trezor-client/src/client/ethereum.rs
Normal file
@ -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<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// 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<u32>) -> Result<String> {
|
||||
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<u8>, path: Vec<u32>) -> Result<Signature> {
|
||||
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<u32>,
|
||||
nonce: Vec<u8>,
|
||||
gas_price: Vec<u8>,
|
||||
gas_limit: Vec<u8>,
|
||||
to: String,
|
||||
value: Vec<u8>,
|
||||
_data: Vec<u8>,
|
||||
chain_id: u64,
|
||||
) -> Result<Signature> {
|
||||
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<u32>,
|
||||
nonce: Vec<u8>,
|
||||
gas_limit: Vec<u8>,
|
||||
to: String,
|
||||
value: Vec<u8>,
|
||||
_data: Vec<u8>,
|
||||
chain_id: u64,
|
||||
max_gas_fee: Vec<u8>,
|
||||
max_priority_fee: Vec<u8>,
|
||||
access_list: Vec<AccessListItem>,
|
||||
) -> Result<Signature> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
241
rust/trezor-client/src/client/mod.rs
Normal file
241
rust/trezor-client/src/client/mod.rs
Normal file
@ -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<protos::Features>,
|
||||
transport: Box<dyn Transport>,
|
||||
}
|
||||
|
||||
/// Create a new Trezor instance with the given transport.
|
||||
pub fn trezor_with_transport(model: Model, transport: Box<dyn Transport>) -> 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<S: TrezorMessage>(&mut self, message: S) -> Result<ProtoMessage> {
|
||||
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<ResultHandler<'a, T, R>>,
|
||||
) -> Result<TrezorResponse<'a, T, R>> {
|
||||
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<Vec<u8>>) -> Result<()> {
|
||||
let features = self.initialize(session_id)?.ok()?;
|
||||
self.features = Some(features);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize(
|
||||
&mut self,
|
||||
session_id: Option<Vec<u8>>,
|
||||
) -> Result<TrezorResponse<'_, Features, Features>> {
|
||||
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<TrezorResponse<'_, (), protos::Success>> {
|
||||
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<TrezorResponse<'_, (), protos::Success>> {
|
||||
let mut req = protos::ChangePin::new();
|
||||
req.set_remove(remove);
|
||||
self.call(req, Box::new(|_, _| Ok(())))
|
||||
}
|
||||
|
||||
pub fn wipe_device(&mut self) -> Result<TrezorResponse<'_, (), protos::Success>> {
|
||||
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<TrezorResponse<'_, (), protos::Success>> {
|
||||
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<TrezorResponse<'_, EntropyRequest<'_>, 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<TrezorResponse<'_, (), protos::Success>> {
|
||||
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<String>,
|
||||
use_passphrase: Option<bool>,
|
||||
homescreen: Option<Vec<u8>>,
|
||||
auto_lock_delay_ms: Option<usize>,
|
||||
) -> Result<TrezorResponse<'_, (), protos::Success>> {
|
||||
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<u8>,
|
||||
curve: String,
|
||||
) -> Result<TrezorResponse<'_, Vec<u8>, 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())))
|
||||
}
|
||||
}
|
92
rust/trezor-client/src/error.rs
Normal file
92
rust/trezor-client/src/error.rs
Normal file
@ -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<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// 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),
|
||||
}
|
334
rust/trezor-client/src/flows/sign_tx.rs
Normal file
334
rust/trezor-client/src/flows/sign_tx.rs
Normal file
@ -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<protos::TxAck> {
|
||||
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<protos::TxAck> {
|
||||
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<protos::TxAck> {
|
||||
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<TrezorResponse<'a, SignTxProgress<'a>, 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<TrezorResponse<'a, SignTxProgress<'a>, 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)
|
||||
}
|
||||
}
|
187
rust/trezor-client/src/lib.rs
Normal file
187
rust/trezor-client/src/lib.rs
Normal file
@ -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<Trezor> {
|
||||
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<AvailableDevice> {
|
||||
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<Trezor> {
|
||||
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");
|
||||
}
|
||||
}
|
300
rust/trezor-client/src/messages.rs
Normal file
300
rust/trezor-client/src/messages.rs
Normal file
@ -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,
|
||||
}
|
49
rust/trezor-client/src/transport/error.rs
Normal file
49
rust/trezor-client/src/transport/error.rs
Normal file
@ -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,
|
||||
}
|
85
rust/trezor-client/src/transport/mod.rs
Normal file
85
rust/trezor-client/src/transport/mod.rs
Normal file
@ -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<u8>);
|
||||
|
||||
impl ProtoMessage {
|
||||
pub fn new(mt: MessageType, payload: Vec<u8>) -> 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<u8> {
|
||||
self.1
|
||||
}
|
||||
|
||||
/// Take the payload from the ProtoMessage and parse it to a protobuf message.
|
||||
pub fn into_message<M: protobuf::Message>(self) -> Result<M, protobuf::Error> {
|
||||
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<ProtoMessage, error::Error>;
|
||||
}
|
||||
|
||||
/// A delegation method to connect an available device transport. It delegates to the different
|
||||
/// transport types.
|
||||
pub fn connect(available_device: &AvailableDevice) -> Result<Box<dyn Transport>, 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<Model> {
|
||||
match dev_id {
|
||||
constants::DEV_TREZOR_LEGACY => Some(Model::TrezorLegacy),
|
||||
constants::DEV_TREZOR => Some(Model::Trezor),
|
||||
constants::DEV_TREZOR_BOOTLOADER => Some(Model::TrezorBootloader),
|
||||
_ => None,
|
||||
}
|
||||
}
|
208
rust/trezor-client/src/transport/protocol.rs
Normal file
208
rust/trezor-client/src/transport/protocol.rs
Normal file
@ -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<u8>) -> Result<(), Error>;
|
||||
fn read_chunk(&mut self) -> Result<Vec<u8>, 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<ProtoMessage, Error>;
|
||||
}
|
||||
|
||||
/// 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<L: Link> {
|
||||
pub link: L,
|
||||
pub session_id: u32,
|
||||
}
|
||||
|
||||
impl<L: Link> Protocol for ProtocolV2<L> {
|
||||
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<ProtoMessage, Error> {
|
||||
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<u8> = 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<L: Link> {
|
||||
pub link: L,
|
||||
}
|
||||
|
||||
impl<L: Link> Protocol for ProtocolV1<L> {
|
||||
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<ProtoMessage, Error> {
|
||||
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<u8> = 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()))
|
||||
}
|
||||
}
|
153
rust/trezor-client/src/transport/udp.rs
Normal file
153
rust/trezor-client/src/transport/udp.rs
Normal file
@ -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<u8>) -> 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<Vec<u8>, 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<UdpLink, Error> {
|
||||
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<bool, Error> {
|
||||
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<UdpLink>,
|
||||
}
|
||||
|
||||
impl UdpTransport {
|
||||
pub fn find_devices(debug: bool, path: Option<&str>) -> Result<Vec<AvailableDevice>, 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<Box<dyn Transport>, 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<ProtoMessage, Error> {
|
||||
self.protocol.read()
|
||||
}
|
||||
}
|
175
rust/trezor-client/src/transport/webusb.rs
Normal file
175
rust/trezor-client/src/transport/webusb.rs
Normal file
@ -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<GlobalContext>,
|
||||
endpoint: u8,
|
||||
}
|
||||
|
||||
impl Link for WebUsbLink {
|
||||
fn write_chunk(&mut self, chunk: Vec<u8>) -> 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<Vec<u8>, 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<WebUsbLink>,
|
||||
}
|
||||
|
||||
impl WebUsbTransport {
|
||||
pub fn find_devices(debug: bool) -> Result<Vec<AvailableDevice>, 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<Box<dyn Transport>, 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<ProtoMessage, Error> {
|
||||
self.protocol.read()
|
||||
}
|
||||
}
|
76
rust/trezor-client/src/utils.rs
Normal file
76
rust/trezor-client/src/utils.rs
Normal file
@ -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<address::Address> {
|
||||
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<sha256d::Hash> {
|
||||
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<RecoverableSignature, bitcoin::secp256k1::Error> {
|
||||
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<String> {
|
||||
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<u32>`.
|
||||
pub fn convert_path(path: &bip32::DerivationPath) -> Vec<u32> {
|
||||
path.into_iter().map(|i| u32::from(*i)).collect()
|
||||
}
|
Loading…
Reference in New Issue
Block a user