diff --git a/tools/hid-bridge/50-hid-bridge.rules b/tools/hid-bridge/50-hid-bridge.rules new file mode 100644 index 000000000..82c637344 --- /dev/null +++ b/tools/hid-bridge/50-hid-bridge.rules @@ -0,0 +1,2 @@ +KERNELS=="uhid", KERNEL=="hidraw*", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" +KERNEL=="uhid", MODE="0660", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" diff --git a/tools/hid-bridge/README.md b/tools/hid-bridge/README.md new file mode 100644 index 000000000..7e5382900 --- /dev/null +++ b/tools/hid-bridge/README.md @@ -0,0 +1,21 @@ +# hid-bridge + +Creates a virtual hid device which can be controlled by a user driver via a UDP port. + +## Installation + +You need Python 3.5 or higher. + +The uhid driver is required. If it is build as a module and not loaded, load it by `modprobe uhid`. + +You must have read/write permission to the `/dev/uhid/` device as same as to the newly created `/dev/hidraw*` device. This may be accomplished by copying `50-hid-bridge.rules` into `/dev/udev/rules.d/`. You may need to reload the driver. + +## Usage + +Run [TREZOR emulator](https://github.com/trezor/trezor-core/blob/master/docs/emulator.md) and `./hid-bridge`. + +## Known issues + +Does not work with Firefox. Firefox closes hid devices on the lost of the focus. + +Does not work with the emulator in the debug mode sice emulator doesn't start hid interface. diff --git a/tools/hid-bridge/hid-bridge b/tools/hid-bridge/hid-bridge new file mode 100755 index 000000000..08575e6c4 --- /dev/null +++ b/tools/hid-bridge/hid-bridge @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import select +import argparse + +from udp_interface import UDPInterface +from hid_interface import HIDInterface +import uhid +import logger + + +parser = argparse.ArgumentParser() +parser.add_argument( + "-e", + default=21325, + metavar="port", + type=int, + help="UDP port the utility communicates with, 21325 by default.", +) +parser.add_argument( + "-l", + "--log-level", + choices=["none", "raw", "uhid-event", "hid-packet"], + default="none", + help="Do not log at all (none); log everything sent to and received by the uhid device and the UDP socket (raw); log uhid events written to and read by the uhid device (uhid-event), log hid packets send to and received by the virtual hid device (hid-packet).", +) +parser.add_argument( + "-t", + "--log-timestams", + dest="log_timestamps", + action="store_true", + default="False", + help="Do include timestamps in the log, this is the default option.", +) +parser.add_argument( + "--no-log-timestams", + dest="log_timestamps", + action="store_false", + help="Do not include timestamps in the log.", +) +args = parser.parse_args() + +logger.log_level = args.log_level +logger.log_timestamps = args.log_timestamps + +udp_interface = UDPInterface(args.e) +hid_interface = HIDInterface() + +poller = select.poll() +poller.register(udp_interface.file_descriptor, select.POLLIN | select.POLLPRI) +poller.register(hid_interface.file_descriptor, select.POLLIN | select.POLLPRI) + +while True: + events = poller.poll() + + for descriptor, event in events: + if descriptor == hid_interface.file_descriptor: + data = hid_interface.process_event() + if data: + udp_interface.write(data) + if descriptor == udp_interface.file_descriptor: + data = udp_interface.read(uhid.EVENT_LENGTH) + hid_interface.write_data(data) diff --git a/tools/hid-bridge/hid_interface.py b/tools/hid-bridge/hid_interface.py new file mode 100644 index 000000000..e963b85c6 --- /dev/null +++ b/tools/hid-bridge/hid_interface.py @@ -0,0 +1,109 @@ +import os + +import logger +import uhid + + +def random_bytes(length): + return os.urandom(length) + + +class HIDInterface: + uhid_device = "/dev/uhid" + + def __init__(self): + self.file_descriptor = os.open(HIDInterface.uhid_device, os.O_RDWR) + self.create_device() + + def __uhid_read(self, length): + data = os.read(self.file_descriptor, length) + logger.log_raw("{} >".format(HIDInterface.uhid_device), data.hex()) + return data + + def __uhid_write(self, data): + bytes_written = os.write(self.file_descriptor, data) + assert bytes_written == len(data) + logger.log_raw("{} <".format(HIDInterface.uhid_device), data.hex()) + + def create_device(self): + name = b"Virtual TREZOR" + phys = b"" + uniq = random_bytes(64) + bus = 0 + vendor = 0x1209 + product = 0x53C1 + version = 0x0200 + country = 0 + # fmt: off + rd_data = bytes([ + 0x06, 0xD0, 0xF1, # USAGE_PAGE (FIDO Alliance) + 0x09, 0x01, # USAGE (U2F HID Authenticator Device) + 0xA1, 0x01, # COLLECTION (Application) + 0x09, 0x20, # USAGE (Input Report Data) + 0x15, 0x00, # LOGICAL_MINIMUM (0) + 0x26, 0xFF, 0x00, # LOGICAL_MAXIMUM (255) + 0x75, 0x08, # REPORT_SIZE (8) + 0x95, 0x40, # REPORT_COUNT (64) + 0x81, 0x02, # INPUT (Data,Var,Abs) + 0x09, 0x21, # USAGE (Output Report Data) + 0x15, 0x00, # LOGICAL_MINIMUM (0) + 0x26, 0xFF, 0x00, # LOGICAL_MAXIMUM (255) + 0x75, 0x08, # REPORT_SIZE (8) + 0x95, 0x40, # REPORT_COUNT (64) + 0x91, 0x02, # OUTPUT (Data,Var,Abs) + 0xC0, # END_COLLECTION + ]) + # fmt: on + + buf = uhid.create_create2_event( + name, phys, uniq, bus, vendor, product, version, country, rd_data + ) + self.__uhid_write(buf) + logger.log_uhid_event( + "UHID_CREATE2", + "name='{}' phys='{}' uniq=0x{} rd_size={} bus=0x{:04x} vendor=0x{:04x} product=0x{:04x} version=0x{:04x} country=0x{:04x} rd_data=0x{}".format( + name.decode("ascii"), + phys.decode("ascii"), + uniq.hex(), + len(rd_data), + bus, + vendor, + product, + version, + country, + rd_data.hex(), + ), + ) + + def write_data(self, data): + buf = uhid.create_input2_event(data) + self.__uhid_write(buf) + logger.log_uhid_event( + "UHID_INPUT2", "data=0x{} size={}".format(data.hex(), len(data)) + ) + logger.log_hid_packet("DEVICE_OUTPUT", "0x{}".format(data.hex())) + + def process_event(self): + ev_type, request = uhid.parse_event(self.__uhid_read(uhid.EVENT_LENGTH)) + if ev_type == uhid.EVENT_TYPE_START: + dev_flags, = request + logger.log_uhid_event("UHID_START", "dev_flags=0b{:08b}".format(dev_flags)) + elif ev_type == uhid.EVENT_TYPE_STOP: + logger.log_uhid_event("UHID_STOP") + elif ev_type == uhid.EVENT_TYPE_OPEN: + logger.log_uhid_event("UHID_OPEN") + elif ev_type == uhid.EVENT_TYPE_CLOSE: + logger.log_uhid_event("UHID_CLOSE") + elif ev_type == uhid.EVENT_TYPE_OUTPUT: + data, size, rtype = request + logger.log_uhid_event( + "UHID_OUTPUT", + "data=0x{} size={} rtype={}".format(data.hex(), size, rtype), + ) + logger.log_hid_packet("DEVICE_INPUT", "0x{}".format(data[1:].hex())) + return data[1:] + else: + logger.log_uhid_event( + "UNKNOWN_EVENT", + "ev_type={} request=0x{}".format(ev_type, request.hex()), + ) diff --git a/tools/hid-bridge/logger.py b/tools/hid-bridge/logger.py new file mode 100644 index 000000000..84df2ac90 --- /dev/null +++ b/tools/hid-bridge/logger.py @@ -0,0 +1,33 @@ +import datetime + +log_level = "None" +log_timestamps = False + + +def __get_timestamp(): + return str(datetime.datetime.now()) + + +def __log_message(message): + if log_timestamps == True: + print("{}\t{}".format(__get_timestamp(), message)) + else: + print(message) + + +def log_uhid_event(event_name, params=None): + if log_level == "uhid-event": + if params: + __log_message("{}\t{}".format(event_name, params)) + else: + __log_message(event_name) + + +def log_hid_packet(packet_name, payload): + if log_level == "hid-packet": + __log_message("{}\t{}".format(packet_name, payload)) + + +def log_raw(direction, payload): + if log_level == "raw": + __log_message("{}\t{}".format(direction, payload)) diff --git a/tools/hid-bridge/udp_interface.py b/tools/hid-bridge/udp_interface.py new file mode 100644 index 000000000..7ef98d870 --- /dev/null +++ b/tools/hid-bridge/udp_interface.py @@ -0,0 +1,39 @@ +import socket + +import logger + + +class UDPInterface: + def __init__(self, destination_port): + self.bind_ip = "127.0.0.1" + self.bind_port = 21423 + + self.destination_ip = "127.0.0.1" + self.destination_port = destination_port + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind((self.bind_ip, self.bind_port)) + + self.file_descriptor = self.socket.fileno() + + def write(self, data): + bytes_sent = self.socket.sendto( + data, ((self.destination_ip, self.destination_port)) + ) + assert bytes_sent == len(data) + logger.log_raw( + "{}:{} < {}:{}".format( + self.destination_ip, self.destination_port, self.bind_ip, self.bind_port + ), + data.hex(), + ) + + def read(self, length): + data, address = self.socket.recvfrom(length) + logger.log_raw( + "{}:{} < {}:{}".format( + self.bind_ip, self.bind_port, address[0], address[1] + ), + data.hex(), + ) + return data diff --git a/tools/hid-bridge/uhid.py b/tools/hid-bridge/uhid.py new file mode 100644 index 000000000..99236152e --- /dev/null +++ b/tools/hid-bridge/uhid.py @@ -0,0 +1,73 @@ +import struct + +EVENT_TYPE_START = 2 +EVENT_TYPE_STOP = 3 +EVENT_TYPE_OPEN = 4 +EVENT_TYPE_CLOSE = 5 +EVENT_TYPE_OUTPUT = 6 +EVENT_TYPE_CREATE2 = 11 +EVENT_TYPE_INTPUT2 = 12 + +DATA_MAX = 4096 +EVENT_LENGTH = 4380 + +INPUT2_REQ_FMT = "< H {}s".format(DATA_MAX) +CREATE2_REQ_FMT = "< 128s 64s 64s H H L L L L {}s".format(DATA_MAX) +START_REQ_FMT = "< Q" +OUTPUT_REQ_FMT = "< {}s H B".format(DATA_MAX) + + +def pack_event(ev_type, request): + return ev_type.to_bytes(4, byteorder="little") + request + + +def unpack_event(buf): + return int.from_bytes(buf[:4], byteorder="little"), buf[4:] + + +def parse_event(event): + assert len(event) == EVENT_LENGTH + + ev_type, request = unpack_event(event) + + if ev_type == EVENT_TYPE_START: + request = struct.unpack_from(START_REQ_FMT, request) + elif ev_type == EVENT_TYPE_STOP: + request = [] + elif ev_type == EVENT_TYPE_OPEN: + request = [] + elif ev_type == EVENT_TYPE_CLOSE: + request = [] + elif ev_type == EVENT_TYPE_OUTPUT: + data, size, rtype = struct.unpack_from(OUTPUT_REQ_FMT, request) + data = data[:size] + request = [data, size, rtype] + + return ev_type, request + + +def create_create2_event( + name, phys, uniq, bus, vendor, product, version, country, rd_data +): + uhid_create2_req = struct.pack( + CREATE2_REQ_FMT, + name, + phys, + uniq, + len(rd_data), + bus, + vendor, + product, + version, + country, + rd_data, + ) + event = pack_event(EVENT_TYPE_CREATE2, uhid_create2_req) + + return event + + +def create_input2_event(data): + uhid_input2_req = struct.pack(INPUT2_REQ_FMT, len(data), data) + event = pack_event(EVENT_TYPE_INTPUT2, uhid_input2_req) + return event