From 06a9251f15d8cd0bacac91a9e9058141b008f4d8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 25 Jun 2021 17:14:00 -0300 Subject: [PATCH] Add LedgerTCP hardware wallet support This communicates with the Ledger over TCP, which is what the ledger emulator requires. To use, specify: --hw-device LedgerTCP --hw-device-address localhost:9999 to the wallet command-line arguments. --- cmake/StaticBuild.cmake | 1 + src/device/CMakeLists.txt | 1 + src/device/device.hpp | 4 + src/device/device_ledger.cpp | 42 ++++++-- src/device/device_ledger.hpp | 18 +++- src/device/io_device.hpp | 2 +- src/device/io_hid.cpp | 2 +- src/device/io_hid.hpp | 2 +- src/device/io_ledger_tcp.cpp | 193 +++++++++++++++++++++++++++++++++++ src/device/io_ledger_tcp.hpp | 41 ++++++++ src/wallet/wallet2.cpp | 7 +- src/wallet/wallet2.h | 7 +- 12 files changed, 304 insertions(+), 16 deletions(-) create mode 100644 src/device/io_ledger_tcp.cpp create mode 100644 src/device/io_ledger_tcp.hpp diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index a13d1a9df..ee5a524f4 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -595,6 +595,7 @@ build_external(zmq ${zmq_patch} CONFIGURE_COMMAND ./configure ${zmq_cross_host} --prefix=${DEPS_DESTDIR} --enable-static --disable-shared --disable-curve-keygen --enable-curve --disable-drafts --disable-libunwind --with-libsodium + --disable-libbsd --disable-perf --without-pgm --without-norm --without-vmci --without-docs --with-pic --disable-Werror "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=-fstack-protector ${deps_CFLAGS}" "CXXFLAGS=-fstack-protector ${deps_CXXFLAGS}" ${cross_extra} diff --git a/src/device/CMakeLists.txt b/src/device/CMakeLists.txt index 2105eb24d..9c638bf51 100644 --- a/src/device/CMakeLists.txt +++ b/src/device/CMakeLists.txt @@ -70,6 +70,7 @@ if (HIDAPI_FOUND) target_sources(device PRIVATE device_ledger.cpp io_hid.cpp + io_ledger_tcp.cpp ) target_link_libraries(device PRIVATE hidapi_libusb) endif() diff --git a/src/device/device.hpp b/src/device/device.hpp index 40fb96c25..3ea559512 100644 --- a/src/device/device.hpp +++ b/src/device/device.hpp @@ -119,6 +119,10 @@ namespace hw { virtual bool set_name(std::string_view name) = 0; virtual std::string get_name() const = 0; + // Optional; can be used to take an address parameter if required (e.g. Ledger TCP uses this + // to specify the TCP address). + virtual void set_address(std::string_view address) {} + virtual bool init() = 0; virtual bool release() = 0; diff --git a/src/device/device_ledger.cpp b/src/device/device_ledger.cpp index 24254f5e9..3699a8d28 100644 --- a/src/device/device_ledger.cpp +++ b/src/device/device_ledger.cpp @@ -28,6 +28,7 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // +#include "device/io_ledger_tcp.hpp" #include "io_hid.hpp" #include "version.h" #include "device_ledger.hpp" @@ -313,7 +314,7 @@ namespace hw::ledger { static_assert(BLAKE2B_HASH_CHUNK_SIZE <= 254, "Max BLAKE2b data chunk size exceeds the protocol limit"); - device_ledger::device_ledger(): hw_device(0x0101, 0x05, 64, 2000) { + device_ledger::device_ledger() : hw_device{std::make_unique(0x0101, 0x05, 64, 2000)} { id = device_id++; reset_buffer(); has_view_key = false; @@ -321,6 +322,14 @@ namespace hw::ledger { MDEBUG("Device " << id << " Created"); } + device_ledger::device_ledger(io::ledger_tcp&& tcp) : hw_device{std::make_unique(std::move(tcp))} { + id = device_id++; + reset_buffer(); + has_view_key = false; + tx_in_progress = false; + MDEBUG("Device " << id << " (tcp) created"); + } + device_ledger::~device_ledger() { release(); MDEBUG("Device " << id << " Destroyed"); @@ -497,7 +506,7 @@ namespace hw::ledger { unsigned int device_ledger::exchange(bool wait_on_input) { logCMD(); - length_recv = hw_device.exchange(buffer_send, length_send, buffer_recv, BUFFER_SEND_SIZE, wait_on_input); + length_recv = hw_device->exchange(buffer_send, length_send, buffer_recv, BUFFER_RECV_SIZE, wait_on_input); CHECK_AND_ASSERT_THROW_MES(length_recv >= 2, "Communication error, less than two bytes received"); length_recv -= 2; @@ -537,12 +546,25 @@ namespace hw::ledger { return name; } + void device_ledger::set_address(std::string_view addr) { + if (addr.empty() || !hw_device) + return; + auto* tcp = dynamic_cast(hw_device.get()); + if (!tcp) + return; + if (auto pos = addr.rfind(':'); pos != addr.npos) { + tcp->port = addr.substr(pos + 1); + addr = addr.substr(0, pos); + } + tcp->host = addr; + } + bool device_ledger::init() { #ifdef DEBUG_HWDEVICE debug_device = &get_device("default"); #endif release(); - hw_device.init(); + hw_device->init(); MDEBUG("Device " << id <<" HIDUSB inited"); return true; } @@ -554,7 +576,12 @@ namespace hw::ledger { bool device_ledger::connect() { disconnect(); - hw_device.connect(known_devices); + if (auto* hid_io = dynamic_cast(hw_device.get())) + hid_io->connect(known_devices); + else if (auto* tcp = dynamic_cast(hw_device.get())) + tcp->connect(); + else + throw std::logic_error{"Invalid ledger hardware configure"}; reset(); check_network_type(); @@ -571,17 +598,17 @@ namespace hw::ledger { } bool device_ledger::connected() const { - return hw_device.connected(); + return hw_device->connected(); } bool device_ledger::disconnect() { - hw_device.disconnect(); + hw_device->disconnect(); return true; } bool device_ledger::release() { disconnect(); - hw_device.release(); + hw_device->release(); return true; } @@ -1943,6 +1970,7 @@ namespace hw::ledger { void register_all(std::map>& registry) { registry.emplace("Ledger", std::make_unique()); + registry.emplace("LedgerTCP", std::make_unique(io::ledger_tcp{})); } #else //WITH_DEVICE_LEDGER diff --git a/src/device/device_ledger.hpp b/src/device/device_ledger.hpp index 7d1a0a07e..ef9c85a4a 100644 --- a/src/device/device_ledger.hpp +++ b/src/device/device_ledger.hpp @@ -34,6 +34,7 @@ #include #include #include "device.hpp" +#include "device/io_ledger_tcp.hpp" #include "log.hpp" #include "io_hid.hpp" @@ -142,7 +143,7 @@ namespace hw::ledger { mutable std::mutex command_locker; //IO - hw::io::hid hw_device; + std::unique_ptr hw_device; unsigned int length_send; unsigned char buffer_send[BUFFER_SEND_SIZE]; unsigned int length_recv; @@ -202,11 +203,20 @@ namespace hw::ledger { #endif public: + // Constructs a ledger device object that connects to a physical ledger device plugged into + // the system. device_ledger(); + + // Constructs a ledger device object that uses a TCP socket to communicate with the ledger + // device, typically for use with a Ledger emulator which doesn't emulated the USB layer. + explicit device_ledger(io::ledger_tcp&& tcp); + ~device_ledger(); - device_ledger(const device_ledger &device) = delete ; - device_ledger& operator=(const device_ledger &device) = delete; + device_ledger(const device_ledger&) = delete ; + device_ledger(device_ledger&&) = delete ; + device_ledger& operator=(const device_ledger&) = delete; + device_ledger&& operator=(device_ledger&&) = delete; bool is_hardware_device() const override { return connected(); } @@ -217,6 +227,8 @@ namespace hw::ledger { /* ======================================================================= */ bool set_name(std::string_view name) override; + void set_address(std::string_view addr) override; + std::string get_name() const override; bool init() override; bool release() override; diff --git a/src/device/io_device.hpp b/src/device/io_device.hpp index 8a4890d0c..b223bec3e 100644 --- a/src/device/io_device.hpp +++ b/src/device/io_device.hpp @@ -46,7 +46,7 @@ namespace hw::io { virtual void disconnect() = 0; virtual bool connected() const = 0; - virtual int exchange(unsigned char *command, unsigned int cmd_len, unsigned char *response, unsigned int max_resp_len, bool user_input) = 0; + virtual int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) = 0; }; } diff --git a/src/device/io_hid.cpp b/src/device/io_hid.cpp index 5da61e979..e64912a64 100644 --- a/src/device/io_hid.cpp +++ b/src/device/io_hid.cpp @@ -140,7 +140,7 @@ namespace hw::io { return usb_device != nullptr; } - int hid::exchange(unsigned char *command, unsigned int cmd_len, unsigned char *response, unsigned int max_resp_len, bool user_input) { + int hid::exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) { unsigned char buffer[400]; unsigned char padding_buffer[MAX_BLOCK+1]; unsigned int result; diff --git a/src/device/io_hid.hpp b/src/device/io_hid.hpp index 2d65b50ce..b82bbce36 100644 --- a/src/device/io_hid.hpp +++ b/src/device/io_hid.hpp @@ -91,7 +91,7 @@ namespace hw { void connect(const std::vector& conn); bool connect(unsigned int vid, unsigned int pid, std::optional interface_number, std::optional usage_page); bool connected() const override; - int exchange(unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override; + int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override; void disconnect() override; void release() override; }; diff --git a/src/device/io_ledger_tcp.cpp b/src/device/io_ledger_tcp.cpp new file mode 100644 index 000000000..764642b99 --- /dev/null +++ b/src/device/io_ledger_tcp.cpp @@ -0,0 +1,193 @@ +#include "io_ledger_tcp.hpp" +#include "common/oxen.h" +#include +#include +#include +#include +#include "epee/misc_log_ex.h" + +extern "C" { +#ifdef _WIN32 +# include +# include +#else +# include +# include +# include +# include +#endif +#include +#include +#include +} + +#undef OXEN_DEFAULT_LOG_CATEGORY +#define OXEN_DEFAULT_LOG_CATEGORY "device.io" + +namespace hw::io { + +static std::string to_string(const addrinfo* a) { + std::array buf; + std::string addr; +#ifdef _WIN32 + unsigned long buflen = buf.size(); + if (auto rc = WSAAddressToString(a->ai_addr, a->ai_addrlen, nullptr, buf.data(), &buflen); + rc == 0) + addr = buf.data(); + else + addr = "[error:"s + std::to_string(rc) + "]"; +#else + if (inet_ntop(a->ai_family, a->ai_addr, buf.data(), buf.size())) + addr = buf.data(); + else + addr = "[error:"s + strerror(errno) + "]"; +#endif + if (a->ai_family == AF_INET) + (addr += ':') += std::to_string(reinterpret_cast(a->ai_addr)->sin_port); + else if (a->ai_family == AF_INET6) + (addr += ':') += std::to_string(reinterpret_cast(a->ai_addr)->sin6_port); + return addr; +} + +void ledger_tcp::connect() { + disconnect(); + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + throw std::runtime_error{"Failed to open socket: "s + strerror(errno)}; + auto closer = oxen::defer([&] { close(fd); }); + +#ifdef _WIN32 + unsigned long blocking_param = 1; // 1 = make non-blocking, 0 = blocking + if (auto result = ioctlsocket(fd, FIONBIO, &blocking_param); + result != NO_ERROR) + throw std::runtime_error{"ioctlsocket failed with error: " + std::to_string(result)}; +#else + if (-1 == fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK)) + throw std::runtime_error{"Failed to set socket non-blocking: "s + strerror(errno)}; +#endif + + addrinfo* addr; + if (int rc = getaddrinfo(host.data(), port.data(), nullptr, &addr); + rc != 0) + throw std::runtime_error{"Failed to resolve " + host + ":" + port + ": " + gai_strerror(rc)}; + auto addr_free = oxen::defer([&] { freeaddrinfo(addr); }); + + const addrinfo* a; + bool connected = false; + const char* err = "An unknown error occurred"; + for (a = addr; a && !connected; a = a->ai_next) { + MDEBUG("Attempting to connect to " << to_string(a)); + int rc = ::connect(fd, a->ai_addr, a->ai_addrlen); + connected = rc == 0; + if (rc == -1) { + if (errno == EINPROGRESS) { + timeval timeo; + timeo.tv_sec = std::chrono::duration_cast(connect_timeout).count(); + timeo.tv_usec = (connect_timeout % 1s).count(); + fd_set myset; + FD_ZERO(&myset); + FD_SET(fd, &myset); + rc = select(fd + 1, nullptr, &myset, nullptr, &timeo); + if (rc > 0) + connected = true; + else if (rc == 0) + err = "Connection timed out"; + else + err = strerror(errno); + } else { + err = strerror(errno); + } + } + } + if (!connected) + throw std::runtime_error{"Failed to connect to " + host + ":" + port + ": " + err}; + + MDEBUG("Connected to " << to_string(a)); + +#ifdef _WIN32 + blocking_param = 0; + if (auto result = ioctlsocket(fd, FIONBIO, &blocking_param); + result != NO_ERROR) + throw std::runtime_error{"ioctlsocket failed with error: " + std::to_string(result)}; +#else + if (-1 == fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK)) + throw std::runtime_error{"Failed to set socket back to blocking: "s + strerror(errno)}; +#endif + + timeval timeo; + timeo.tv_sec = std::chrono::duration_cast(exchange_timeout).count(); + timeo.tv_usec = (exchange_timeout % 1s).count(); + + // The reinterpret_cast here is needed for Windows's shitty imitation of the api + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeo), sizeof(timeo)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&timeo), sizeof(timeo)); + + sockfd = std::make_unique(fd); + closer.cancel(); +} + +void ledger_tcp::disconnect() { + if (!sockfd) + return; + + close(*sockfd); + sockfd.reset(); +} + +ledger_tcp::~ledger_tcp() { + disconnect(); +} + +bool ledger_tcp::connected() const { + return (bool) sockfd; +} + +void full_read(int fd, unsigned char* to, int size) { + while (size > 0) { + auto read_size = read(fd, to, size); + if (read_size == -1) + throw std::runtime_error{"Failed to read from hardware wallet socket: "s + strerror(errno)}; + size -= read_size; + to += read_size; + } +} + +void full_write(int fd, const unsigned char* from, int size) { + while (size > 0) { + auto wrote = write(fd, from, size); + if (wrote == -1) + throw std::runtime_error{"Failed to write to hardware wallet socket: "s + strerror(errno)}; + size -= wrote; + from += wrote; + } +} + +int ledger_tcp::exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) { + if (!sockfd) + throw std::runtime_error{"Unable to exchange data with hardware wallet: not connected"}; + + // Sending: [SIZE][DATA], where SIZE is a uint32_t in network order + uint32_t size = boost::endian::native_to_big(cmd_len); + const unsigned char* size_bytes = reinterpret_cast(&size); + full_write(*sockfd, size_bytes, 4); + full_write(*sockfd, command, cmd_len); + + + // Receiving: [SIZE][DATA], where SIZE is the length of DATA minus 2 (WTF) because the last two + // bytes of DATA are a 2-byte, u16 status code and... therefore not... included. Good job, Ledger + // devs. + full_read(*sockfd, reinterpret_cast(&size), 4); + auto data_size = boost::endian::big_to_native(size) + 2; + + if (data_size > max_resp_len) + throw std::runtime_error{"Hardware wallet returned unexpectedly large response: got " + + std::to_string(data_size) + " bytes, expected <= " + std::to_string(max_resp_len)}; + + full_read(*sockfd, response, data_size); + + return data_size; +} + +} + diff --git a/src/device/io_ledger_tcp.hpp b/src/device/io_ledger_tcp.hpp new file mode 100644 index 000000000..1dc2a3b78 --- /dev/null +++ b/src/device/io_ledger_tcp.hpp @@ -0,0 +1,41 @@ +// TCP APDU interface, as used by Ledger's emulator system (Speculos). + +#pragma once + +#include +#include +#include +#include "io_device.hpp" + +#pragma once + +namespace hw::io { + +using namespace std::literals; + +class ledger_tcp : public device { + + std::unique_ptr sockfd; + +public: + std::string host = "localhost"; + std::string port = "9999"; + + std::chrono::microseconds connect_timeout = 10s; + std::chrono::microseconds exchange_timeout = 120s; + + ledger_tcp() = default; + ~ledger_tcp() override; + + ledger_tcp(ledger_tcp&&) = default; + ledger_tcp& operator=(ledger_tcp&&) = default; + + void init() override {} + void release() override {} + void connect(); + bool connected() const override; + int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override; + void disconnect() override; +}; + +} diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index f2f4f0608..d2b67bc91 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -282,6 +282,7 @@ struct options { }; const command_line::arg_descriptor kdf_rounds = {"kdf-rounds", tools::wallet2::tr("Number of rounds for the key derivation function"), 1}; const command_line::arg_descriptor hw_device = {"hw-device", tools::wallet2::tr("HW device to use"), ""}; + const command_line::arg_descriptor hw_device_address = {"hw-device-address", tools::wallet2::tr("HW device address, if required"), ""}; const command_line::arg_descriptor hw_device_derivation_path = {"hw-device-deriv-path", tools::wallet2::tr("HW device wallet derivation path (e.g., SLIP-10)"), ""}; const command_line::arg_descriptor tx_notify = { "tx-notify" , "Run a program for each new incoming transaction, '%s' will be replaced by the transaction hash" , "" }; const command_line::arg_descriptor offline = {"offline", tools::wallet2::tr("Do not connect to a daemon"), false}; @@ -345,6 +346,7 @@ std::unique_ptr make_basic(const boost::program_options::variabl auto daemon_port = command_line::get_arg(vm, opts.daemon_port); auto device_name = command_line::get_arg(vm, opts.hw_device); + auto device_addr = command_line::get_arg(vm, opts.hw_device_address); auto device_derivation_path = command_line::get_arg(vm, opts.hw_device_derivation_path); THROW_WALLET_EXCEPTION_IF(!daemon_address.empty() && (!daemon_host.empty() || 0 != daemon_port), @@ -419,6 +421,7 @@ std::unique_ptr make_basic(const boost::program_options::variabl wallet->get_message_store().set_options(vm); #endif wallet->device_name(device_name); + wallet->device_address(device_addr); wallet->device_derivation_path(device_derivation_path); wallet->m_long_poll_disabled = command_line::get_arg(vm, opts.disable_rpc_long_poll); wallet->m_http_client.set_https_client_cert(command_line::get_arg(vm, opts.daemon_ssl_certificate), command_line::get_arg(vm, opts.daemon_ssl_private_key)); @@ -1151,6 +1154,7 @@ void wallet2::init_options(boost::program_options::options_description& desc_par mms::message_store::init_options(desc_params); #endif command_line::add_arg(desc_params, opts.hw_device); + command_line::add_arg(desc_params, opts.hw_device_address); command_line::add_arg(desc_params, opts.hw_device_derivation_path); command_line::add_arg(desc_params, opts.tx_notify); command_line::add_arg(desc_params, opts.offline); @@ -1366,6 +1370,7 @@ bool wallet2::reconnect_device() bool r = true; hw::device &hwdev = lookup_device(m_device_name); hwdev.set_name(m_device_name); + hwdev.set_address(m_device_address); hwdev.set_network_type(m_nettype); hwdev.set_derivation_path(m_device_derivation_path); hwdev.set_callback(get_device_callback()); @@ -7887,7 +7892,7 @@ void wallet2::register_devices(){ hw::trezor::register_all(); } -hw::device& wallet2::lookup_device(const std::string & device_descriptor){ +hw::device& wallet2::lookup_device(const std::string & device_descriptor) { if (!m_devices_registered){ m_devices_registered = true; register_devices(); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 3adfad25e..26c062ede 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1037,8 +1037,10 @@ private: void track_uses(bool value) { m_track_uses = value; } std::chrono::seconds inactivity_lock_timeout() const { return m_inactivity_lock_timeout; } void inactivity_lock_timeout(std::chrono::seconds seconds) { m_inactivity_lock_timeout = seconds; } - const std::string & device_name() const { return m_device_name; } - void device_name(const std::string & device_name) { m_device_name = device_name; } + const std::string& device_name() const { return m_device_name; } + const std::string& device_address() const { return m_device_address; } + void device_name(std::string device_name) { m_device_name = std::move(device_name); } + void device_address(std::string device_address) { m_device_address = std::move(device_address); } const std::string & device_derivation_path() const { return m_device_derivation_path; } void device_derivation_path(const std::string &device_derivation_path) { m_device_derivation_path = device_derivation_path; } const ExportFormat & export_format() const { return m_export_format; } @@ -1682,6 +1684,7 @@ private: std::unordered_set m_scanned_pool_txs[2]; size_t m_subaddress_lookahead_major, m_subaddress_lookahead_minor; std::string m_device_name; + std::string m_device_address; std::string m_device_derivation_path; uint64_t m_device_last_key_image_sync; bool m_offline;