1
0
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:
wszdexdrf 2023-06-05 11:20:39 +05:30 committed by matejcik
parent 20c9d81018
commit 07ba960ab4
29 changed files with 4042 additions and 0 deletions

View File

@ -0,0 +1 @@
msrv = "1.60"

1
rust/trezor-client/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

885
rust/trezor-client/Cargo.lock generated Normal file
View 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"

View 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
View 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.

View 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

View 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()
}

View 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()
}

View 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());
}
}
}

View 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()
}

View 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);
}

View 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));
}

View 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_*"]

View 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()

View 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

View 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>(),
}
}

View 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(())))
}
}

View 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(),
})
}
}

View 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())))
}
}

View 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),
}

View 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)
}
}

View 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");
}
}

View 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,
}

View 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,
}

View 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,
}
}

View 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()))
}
}

View 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()
}
}

View 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()
}
}

View 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()
}