diff --git a/completion/zsh/_libinput b/completion/zsh/_libinput index 617bc165..3eb32f2e 100644 --- a/completion/zsh/_libinput +++ b/completion/zsh/_libinput @@ -223,6 +223,7 @@ __all_seats() '--multiple[Record multiple devices at once]' \ '--show-keycodes[Show keycodes as-is in the recording]' \ '--with-libinput[Record libinput events alongside device events]' \ + '--with-hidraw[Record hidraw events alongside device events]' \ '*::device:_files -W /dev/input/ -P /dev/input/' } diff --git a/tools/libinput-record.c b/tools/libinput-record.c index 1e763f36..119aac23 100644 --- a/tools/libinput-record.c +++ b/tools/libinput-record.c @@ -70,7 +70,7 @@ enum indent { I_LIBINPUTDEV = 4, /* nodes inside libinput: (the device description */ I_EVENTTYPE = 4, /* event type (evdev:, libinput:, - ...) */ + hidraw:) */ I_EVENT = 6, /* event data */ }; @@ -82,6 +82,7 @@ struct record_device { struct libevdev *evdev_prev; /* previous value, used for EV_ABS deltas */ struct libinput_device *device; + struct list hidraw_devices; struct { bool is_touch_device; @@ -92,6 +93,13 @@ struct record_device { FILE *fp; }; +struct hidraw { + struct list link; + struct record_device *device; + int fd; + char *name; +}; + struct record_context { int timeout; bool show_keycodes; @@ -1208,6 +1216,55 @@ print_libinput_event(struct record_device *dev, struct libinput_event *e) } } +static bool +handle_hidraw(struct hidraw *hidraw) +{ + struct record_device *d = hidraw->device; + unsigned char report[4096]; + const char *sep = ""; + struct timespec ts; + struct timeval tv; + uint64_t time; + + int rc = read(hidraw->fd, report, sizeof(report)); + if (rc <= 0) + return false; + + /* hidraw doesn't give us a timestamps, we have to make them up */ + clock_gettime(CLOCK_MONOTONIC, &ts); + time = s2us(ts.tv_sec) + ns2us(ts.tv_nsec); + + /* The first evdev event is guaranteed to have an event time earlier + than now, so we don't set the offset here, we rely on the evdev + events to do so. This potentially leaves us with multiple hidraw + events at timestap 0 but it's too niche to worry about. */ + if (d->ctx->offset == 0) + time = 0; + else + time = time_offset(d->ctx, time); + + tv = us2tv(time); + + iprintf(d->fp, I_EVENTTYPE, "- hid:\n"); + iprintf(d->fp, I_EVENT, "time: [%3lu, %6lu]\n", tv.tv_sec, tv.tv_usec); + iprintf(d->fp, I_EVENT, "%s: [", hidraw->name); + + for (int byte = 0; byte < rc; byte++) { + if (byte % 16 == 0) { + iprintf(d->fp, I_NONE, "%s\n", sep); + iprintf(d->fp, I_EVENT, " "); + iprintf(d->fp, I_NONE, "0x%02x", report[byte]); + } else { + iprintf(d->fp, I_NONE, "%s0x%02x", sep, report[byte]); + } + sep = ", "; + } + iprintf(d->fp, I_NONE, "\n"); + iprintf(d->fp, I_EVENT, "]\n"); + + return true; +} + static bool handle_libinput_events(struct record_context *ctx, struct record_device *d, @@ -2087,6 +2144,16 @@ libinput_ctx_dispatch(struct record_context *ctx, int fd, void *data) handle_libinput_events(ctx, ctx->first_device, true); } +static void +hidraw_dispatch(struct record_context *ctx, int fd, void *data) +{ + struct hidraw *hidraw = data; + + ctx->had_events = true; + ctx->timestamps.had_events_since_last_time = true; + handle_hidraw(hidraw); +} + static int dispatch_sources(struct record_context *ctx) { @@ -2136,7 +2203,13 @@ mainloop(struct record_context *ctx) arm_timer(timerfd); list_for_each(d, &ctx->devices, link) { + struct hidraw *hidraw; + add_source(ctx, libevdev_get_fd(d->evdev), evdev_dispatch, d); + + list_for_each(hidraw, &d->hidraw_devices, link) { + add_source(ctx, hidraw->fd, hidraw_dispatch, hidraw); + } } if (ctx->libinput) { @@ -2291,6 +2364,8 @@ init_device(struct record_context *ctx, const char *path, bool grab) d->ctx = ctx; d->devnode = safe_strdup(path); + list_init(&d->hidraw_devices); + fd = open(d->devnode, O_RDONLY|O_NONBLOCK); if (fd < 0) { fprintf(stderr, @@ -2389,6 +2464,52 @@ init_libinput(struct record_context *ctx) return true; } +static bool +init_hidraw(struct record_context *ctx) +{ + struct record_device *dev; + + list_for_each(dev, &ctx->devices, link) { + char syspath[PATH_MAX]; + DIR *dir; + struct dirent *entry; + + snprintf(syspath, + sizeof(syspath), + "/sys/class/input/%s/device/device/hidraw", + safe_basename(dev->devnode)); + dir = opendir(syspath); + if (!dir) + continue; + + while ((entry = readdir(dir))) { + char hidraw_node[PATH_MAX]; + int fd; + struct hidraw *hidraw = NULL; + + if (!strstartswith(entry->d_name, "hidraw")) + continue; + + snprintf(hidraw_node, + sizeof(hidraw_node), + "/dev/%s", + entry->d_name); + fd = open(hidraw_node, O_RDONLY|O_NONBLOCK); + if (fd == -1) + continue; + + hidraw = zalloc(sizeof(*hidraw)); + hidraw->fd = fd; + hidraw->name = strdup(entry->d_name); + hidraw->device = dev; + list_insert(&dev->hidraw_devices, &hidraw->link); + } + closedir(dir); + } + + return true; +} + static void usage(void) { @@ -2506,6 +2627,7 @@ enum options { OPT_MULTIPLE, OPT_ALL, OPT_LIBINPUT, + OPT_HIDRAW, OPT_GRAB, }; @@ -2524,12 +2646,16 @@ main(int argc, char **argv) { "all", no_argument, 0, OPT_ALL }, { "help", no_argument, 0, OPT_HELP }, { "with-libinput", no_argument, 0, OPT_LIBINPUT }, + { "with-hidraw", no_argument, 0, OPT_HIDRAW }, { "grab", no_argument, 0, OPT_GRAB }, { 0, 0, 0, 0 }, }; struct record_device *d; const char *output_arg = NULL; - bool all = false, with_libinput = false, grab = false; + bool all = false, + with_libinput = false, + with_hidraw = false, + grab = false; int ndevices; int rc = EXIT_FAILURE; char **paths = NULL; @@ -2575,6 +2701,10 @@ main(int argc, char **argv) case OPT_LIBINPUT: with_libinput = true; break; + case OPT_HIDRAW: + with_hidraw = true; + fprintf(stderr, "# WARNING: do not type passwords while recording HID reports\n"); + break; case OPT_GRAB: grab = true; break; @@ -2650,10 +2780,22 @@ main(int argc, char **argv) if (with_libinput && !init_libinput(&ctx)) goto out; + if (with_hidraw && !init_hidraw(&ctx)) + goto out; + rc = mainloop(&ctx); out: strv_free(paths); list_for_each_safe(d, &ctx.devices, link) { + struct hidraw *hidraw; + + list_for_each_safe(hidraw, &d->hidraw_devices, link) { + close(hidraw->fd); + list_remove(&hidraw->link); + free(hidraw->name); + free(hidraw); + } + if (d->device) libinput_device_unref(d->device); free(d->devnode); diff --git a/tools/libinput-record.man b/tools/libinput-record.man index 4d70c9b5..f2732018 100644 --- a/tools/libinput-record.man +++ b/tools/libinput-record.man @@ -54,7 +54,7 @@ greater than 0. .PD 1 Specifies the output file to use. If \fB\-\-autorestart\fR is given, the filename is used as prefix only. -Where \-\-output-file is not given and the first \fBor\fR last argument is +Where \-\-output-file is not given and the first \fBor\fR last argument is not an input device, the first \fBor\fR last argument will be the output file. .TP 8 @@ -72,6 +72,13 @@ Record libinput events alongside device events. See section .B RECORDING LIBINPUT EVENTS for more details. +.TP 8 +.B \-\-with-hidraw +Record hidraw events alongside device events. +.B DO NOT TYPE SENSITIVE DATA. +See +.B RECORDING HID REPORTS +for more details. .SH RECORDING MULTIPLE DEVICES Sometimes it is necessary to record the events from multiple devices @@ -108,6 +115,15 @@ Note that the libinput context created by \fBlibinput\-record\fR does not affect the running desktop session and does not (can not!) copy any configuration options from that session. +.SH RECORDING HID REPORTS +When the \fB\-\-with-hidraw\fR commandline option is given, +\fBlibinput\-record\fR searches for the hidraw node(s) of the given devices +and prints any incoming HID reports from those devices. +.PP +HID reports are \fBnot obfuscated\fR and a sufficiently +motivated person could recover the key strokes from the logs. Do not type +passwords while recording HID reports. + .SH FILE FORMAT The output file format is in YAML and intended to be both human-readable and machine-parseable. Below is a short example YAML file, all keys are detailed @@ -167,6 +183,9 @@ devices: - ModelAppleTouchpad=1 - AttrSizeHint=32x32 events: + - hid: + time: [ 0, 0] + hidraw0: [1, 2, 3, 4] - evdev: - [ 0, 0, 3, 57, 1420] # EV_ABS / ABS_MT_TRACKING_ID 1420 - [ 0, 0, 3, 53, 1218] # EV_ABS / ABS_MT_POSITION_X 1218 @@ -290,7 +309,15 @@ Each \fBevdev\fR dictionary contains the contents of a \fBstruct input_event\fR in decimal format. The last item in the list is always the \fBSYN_REPORT\fR of this event frame. The next event frame starts a new \fBevdev\fR dictionary entry in the parent \fBevents\fR list. - +.TP 8 +.B { hid: "hidrawX": [ 12, 34, 56 ], ...] } +The \fBhid\fR dictionary contains the hid reports in decimal format, with +the hidraw node as key. The special key \fBtime\fR denotes the current time +when the report was read from the kernel. +.PP +Note that the kernel does not provide timestamps for hidraw events and the +timestamps provided are from \fBclock_gettime(3)\fR. They may be greater +than a subsequent evdev event's timestamp. .SH NOTES .PP This tool records events from the kernel and is independent of libinput. In diff --git a/tools/test_tool_option_parsing.py b/tools/test_tool_option_parsing.py index 5c92b11a..f1241ddb 100755 --- a/tools/test_tool_option_parsing.py +++ b/tools/test_tool_option_parsing.py @@ -309,7 +309,9 @@ def test_libinput_debug_gui_verbose(libinput_debug_gui): libinput_debug_gui.run_command_success(["--verbose"]) -@pytest.mark.parametrize("arg", ["--help", "--show-keycodes", "--with-libinput"]) +@pytest.mark.parametrize( + "arg", ["--help", "--show-keycodes", "--with-libinput", "--with-hidraw"] +) def test_libinput_record_args(libinput_record, arg): libinput_record.run_command_success([arg])