summaryrefslogtreecommitdiffstats
path: root/src/hid_linux.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/hid_linux.c')
-rw-r--r--src/hid_linux.c391
1 files changed, 391 insertions, 0 deletions
diff --git a/src/hid_linux.c b/src/hid_linux.c
new file mode 100644
index 0000000..841a95b
--- /dev/null
+++ b/src/hid_linux.c
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2019-2022 Yubico AB. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/types.h>
+#include <sys/file.h>
+#include <sys/ioctl.h>
+
+#include <linux/hidraw.h>
+#include <linux/input.h>
+
+#include <errno.h>
+#include <libudev.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "fido.h"
+
+struct hid_linux {
+ int fd;
+ size_t report_in_len;
+ size_t report_out_len;
+ sigset_t sigmask;
+ const sigset_t *sigmaskp;
+};
+
+static int
+get_report_descriptor(int fd, struct hidraw_report_descriptor *hrd)
+{
+ int s = -1;
+
+ if (ioctl(fd, IOCTL_REQ(HIDIOCGRDESCSIZE), &s) == -1) {
+ fido_log_error(errno, "%s: ioctl HIDIOCGRDESCSIZE", __func__);
+ return (-1);
+ }
+
+ if (s < 0 || (unsigned)s > HID_MAX_DESCRIPTOR_SIZE) {
+ fido_log_debug("%s: HIDIOCGRDESCSIZE %d", __func__, s);
+ return (-1);
+ }
+
+ hrd->size = (unsigned)s;
+
+ if (ioctl(fd, IOCTL_REQ(HIDIOCGRDESC), hrd) == -1) {
+ fido_log_error(errno, "%s: ioctl HIDIOCGRDESC", __func__);
+ return (-1);
+ }
+
+ return (0);
+}
+
+static bool
+is_fido(const char *path)
+{
+ int fd = -1;
+ uint32_t usage_page = 0;
+ struct hidraw_report_descriptor *hrd = NULL;
+
+ if ((hrd = calloc(1, sizeof(*hrd))) == NULL ||
+ (fd = fido_hid_unix_open(path)) == -1)
+ goto out;
+ if (get_report_descriptor(fd, hrd) < 0 ||
+ fido_hid_get_usage(hrd->value, hrd->size, &usage_page) < 0)
+ usage_page = 0;
+
+out:
+ free(hrd);
+
+ if (fd != -1 && close(fd) == -1)
+ fido_log_error(errno, "%s: close", __func__);
+
+ return (usage_page == 0xf1d0);
+}
+
+static int
+parse_uevent(const char *uevent, int *bus, int16_t *vendor_id,
+ int16_t *product_id)
+{
+ char *cp;
+ char *p;
+ char *s;
+ int ok = -1;
+ short unsigned int x;
+ short unsigned int y;
+ short unsigned int z;
+
+ if ((s = cp = strdup(uevent)) == NULL)
+ return (-1);
+
+ while ((p = strsep(&cp, "\n")) != NULL && *p != '\0') {
+ if (strncmp(p, "HID_ID=", 7) == 0) {
+ if (sscanf(p + 7, "%hx:%hx:%hx", &x, &y, &z) == 3) {
+ *bus = (int)x;
+ *vendor_id = (int16_t)y;
+ *product_id = (int16_t)z;
+ ok = 0;
+ break;
+ }
+ }
+ }
+
+ free(s);
+
+ return (ok);
+}
+
+static char *
+get_parent_attr(struct udev_device *dev, const char *subsystem,
+ const char *devtype, const char *attr)
+{
+ struct udev_device *parent;
+ const char *value;
+
+ if ((parent = udev_device_get_parent_with_subsystem_devtype(dev,
+ subsystem, devtype)) == NULL || (value =
+ udev_device_get_sysattr_value(parent, attr)) == NULL)
+ return (NULL);
+
+ return (strdup(value));
+}
+
+static char *
+get_usb_attr(struct udev_device *dev, const char *attr)
+{
+ return (get_parent_attr(dev, "usb", "usb_device", attr));
+}
+
+static int
+copy_info(fido_dev_info_t *di, struct udev *udev,
+ struct udev_list_entry *udev_entry)
+{
+ const char *name;
+ const char *path;
+ char *uevent = NULL;
+ struct udev_device *dev = NULL;
+ int bus = 0;
+ int ok = -1;
+
+ memset(di, 0, sizeof(*di));
+
+ if ((name = udev_list_entry_get_name(udev_entry)) == NULL ||
+ (dev = udev_device_new_from_syspath(udev, name)) == NULL ||
+ (path = udev_device_get_devnode(dev)) == NULL ||
+ is_fido(path) == 0)
+ goto fail;
+
+ if ((uevent = get_parent_attr(dev, "hid", NULL, "uevent")) == NULL ||
+ parse_uevent(uevent, &bus, &di->vendor_id, &di->product_id) < 0) {
+ fido_log_debug("%s: uevent", __func__);
+ goto fail;
+ }
+
+#ifndef FIDO_HID_ANY
+ if (bus != BUS_USB) {
+ fido_log_debug("%s: bus", __func__);
+ goto fail;
+ }
+#endif
+
+ di->path = strdup(path);
+ if ((di->manufacturer = get_usb_attr(dev, "manufacturer")) == NULL)
+ di->manufacturer = strdup("");
+ if ((di->product = get_usb_attr(dev, "product")) == NULL)
+ di->product = strdup("");
+ if (di->path == NULL || di->manufacturer == NULL || di->product == NULL)
+ goto fail;
+
+ ok = 0;
+fail:
+ if (dev != NULL)
+ udev_device_unref(dev);
+
+ free(uevent);
+
+ if (ok < 0) {
+ free(di->path);
+ free(di->manufacturer);
+ free(di->product);
+ explicit_bzero(di, sizeof(*di));
+ }
+
+ return (ok);
+}
+
+int
+fido_hid_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen)
+{
+ struct udev *udev = NULL;
+ struct udev_enumerate *udev_enum = NULL;
+ struct udev_list_entry *udev_list;
+ struct udev_list_entry *udev_entry;
+ int r = FIDO_ERR_INTERNAL;
+
+ *olen = 0;
+
+ if (ilen == 0)
+ return (FIDO_OK); /* nothing to do */
+
+ if (devlist == NULL)
+ return (FIDO_ERR_INVALID_ARGUMENT);
+
+ if ((udev = udev_new()) == NULL ||
+ (udev_enum = udev_enumerate_new(udev)) == NULL)
+ goto fail;
+
+ if (udev_enumerate_add_match_subsystem(udev_enum, "hidraw") < 0 ||
+ udev_enumerate_scan_devices(udev_enum) < 0)
+ goto fail;
+
+ if ((udev_list = udev_enumerate_get_list_entry(udev_enum)) == NULL) {
+ r = FIDO_OK; /* zero hidraw devices */
+ goto fail;
+ }
+
+ udev_list_entry_foreach(udev_entry, udev_list) {
+ if (copy_info(&devlist[*olen], udev, udev_entry) == 0) {
+ devlist[*olen].io = (fido_dev_io_t) {
+ fido_hid_open,
+ fido_hid_close,
+ fido_hid_read,
+ fido_hid_write,
+ };
+ if (++(*olen) == ilen)
+ break;
+ }
+ }
+
+ r = FIDO_OK;
+fail:
+ if (udev_enum != NULL)
+ udev_enumerate_unref(udev_enum);
+ if (udev != NULL)
+ udev_unref(udev);
+
+ return (r);
+}
+
+void *
+fido_hid_open(const char *path)
+{
+ struct hid_linux *ctx;
+ struct hidraw_report_descriptor *hrd;
+ struct timespec tv_pause;
+ long interval_ms, retries = 0;
+ bool looped;
+
+retry:
+ looped = false;
+
+ if ((ctx = calloc(1, sizeof(*ctx))) == NULL ||
+ (ctx->fd = fido_hid_unix_open(path)) == -1) {
+ free(ctx);
+ return (NULL);
+ }
+
+ while (flock(ctx->fd, LOCK_EX|LOCK_NB) == -1) {
+ if (errno != EWOULDBLOCK) {
+ fido_log_error(errno, "%s: flock", __func__);
+ fido_hid_close(ctx);
+ return (NULL);
+ }
+ looped = true;
+ if (retries++ >= 20) {
+ fido_log_debug("%s: flock timeout", __func__);
+ fido_hid_close(ctx);
+ return (NULL);
+ }
+ interval_ms = retries * 100000000L;
+ tv_pause.tv_sec = interval_ms / 1000000000L;
+ tv_pause.tv_nsec = interval_ms % 1000000000L;
+ if (nanosleep(&tv_pause, NULL) == -1) {
+ fido_log_error(errno, "%s: nanosleep", __func__);
+ fido_hid_close(ctx);
+ return (NULL);
+ }
+ }
+
+ if (looped) {
+ fido_log_debug("%s: retrying", __func__);
+ fido_hid_close(ctx);
+ goto retry;
+ }
+
+ if ((hrd = calloc(1, sizeof(*hrd))) == NULL ||
+ get_report_descriptor(ctx->fd, hrd) < 0 ||
+ fido_hid_get_report_len(hrd->value, hrd->size, &ctx->report_in_len,
+ &ctx->report_out_len) < 0 || ctx->report_in_len == 0 ||
+ ctx->report_out_len == 0) {
+ fido_log_debug("%s: using default report sizes", __func__);
+ ctx->report_in_len = CTAP_MAX_REPORT_LEN;
+ ctx->report_out_len = CTAP_MAX_REPORT_LEN;
+ }
+
+ free(hrd);
+
+ return (ctx);
+}
+
+void
+fido_hid_close(void *handle)
+{
+ struct hid_linux *ctx = handle;
+
+ if (close(ctx->fd) == -1)
+ fido_log_error(errno, "%s: close", __func__);
+
+ free(ctx);
+}
+
+int
+fido_hid_set_sigmask(void *handle, const fido_sigset_t *sigmask)
+{
+ struct hid_linux *ctx = handle;
+
+ ctx->sigmask = *sigmask;
+ ctx->sigmaskp = &ctx->sigmask;
+
+ return (FIDO_OK);
+}
+
+int
+fido_hid_read(void *handle, unsigned char *buf, size_t len, int ms)
+{
+ struct hid_linux *ctx = handle;
+ ssize_t r;
+
+ if (len != ctx->report_in_len) {
+ fido_log_debug("%s: len %zu", __func__, len);
+ return (-1);
+ }
+
+ if (fido_hid_unix_wait(ctx->fd, ms, ctx->sigmaskp) < 0) {
+ fido_log_debug("%s: fd not ready", __func__);
+ return (-1);
+ }
+
+ if ((r = read(ctx->fd, buf, len)) == -1) {
+ fido_log_error(errno, "%s: read", __func__);
+ return (-1);
+ }
+
+ if (r < 0 || (size_t)r != len) {
+ fido_log_debug("%s: %zd != %zu", __func__, r, len);
+ return (-1);
+ }
+
+ return ((int)r);
+}
+
+int
+fido_hid_write(void *handle, const unsigned char *buf, size_t len)
+{
+ struct hid_linux *ctx = handle;
+ ssize_t r;
+
+ if (len != ctx->report_out_len + 1) {
+ fido_log_debug("%s: len %zu", __func__, len);
+ return (-1);
+ }
+
+ if ((r = write(ctx->fd, buf, len)) == -1) {
+ fido_log_error(errno, "%s: write", __func__);
+ return (-1);
+ }
+
+ if (r < 0 || (size_t)r != len) {
+ fido_log_debug("%s: %zd != %zu", __func__, r, len);
+ return (-1);
+ }
+
+ return ((int)r);
+}
+
+size_t
+fido_hid_report_in_len(void *handle)
+{
+ struct hid_linux *ctx = handle;
+
+ return (ctx->report_in_len);
+}
+
+size_t
+fido_hid_report_out_len(void *handle)
+{
+ struct hid_linux *ctx = handle;
+
+ return (ctx->report_out_len);
+}