diff options
Diffstat (limited to 'src/hid_openbsd.c')
-rw-r--r-- | src/hid_openbsd.c | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/src/hid_openbsd.c b/src/hid_openbsd.c new file mode 100644 index 0000000..8b92bd6 --- /dev/null +++ b/src/hid_openbsd.c | |||
@@ -0,0 +1,277 @@ | |||
1 | /* | ||
2 | * Copyright (c) 2019 Google LLC. All rights reserved. | ||
3 | * Use of this source code is governed by a BSD-style | ||
4 | * license that can be found in the LICENSE file. | ||
5 | */ | ||
6 | |||
7 | #include <sys/types.h> | ||
8 | |||
9 | #include <sys/ioctl.h> | ||
10 | #include <dev/usb/usb.h> | ||
11 | #include <dev/usb/usbhid.h> | ||
12 | |||
13 | #include <errno.h> | ||
14 | #include <fcntl.h> | ||
15 | #include <string.h> | ||
16 | #include <unistd.h> | ||
17 | #include <usbhid.h> | ||
18 | #include <poll.h> | ||
19 | |||
20 | #include "fido.h" | ||
21 | |||
22 | #define MAX_UHID 64 | ||
23 | #define MAX_REPORT_LEN (sizeof(((struct usb_ctl_report *)(NULL))->ucr_data)) | ||
24 | |||
25 | struct hid_openbsd { | ||
26 | int fd; | ||
27 | size_t report_in_len; | ||
28 | size_t report_out_len; | ||
29 | }; | ||
30 | |||
31 | int | ||
32 | fido_dev_info_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen) | ||
33 | { | ||
34 | size_t i; | ||
35 | char path[64]; | ||
36 | int is_fido, fd; | ||
37 | struct usb_device_info udi; | ||
38 | report_desc_t rdesc = NULL; | ||
39 | hid_data_t hdata = NULL; | ||
40 | hid_item_t hitem; | ||
41 | fido_dev_info_t *di; | ||
42 | |||
43 | if (ilen == 0) | ||
44 | return (FIDO_OK); /* nothing to do */ | ||
45 | |||
46 | if (devlist == NULL || olen == NULL) | ||
47 | return (FIDO_ERR_INVALID_ARGUMENT); | ||
48 | |||
49 | for (i = *olen = 0; i < MAX_UHID && *olen < ilen; i++) { | ||
50 | snprintf(path, sizeof(path), "/dev/uhid%zu", i); | ||
51 | if ((fd = open(path, O_RDWR)) == -1) { | ||
52 | if (errno != ENOENT && errno != ENXIO) { | ||
53 | fido_log_debug("%s: open %s: %s", __func__, | ||
54 | path, strerror(errno)); | ||
55 | } | ||
56 | continue; | ||
57 | } | ||
58 | memset(&udi, 0, sizeof(udi)); | ||
59 | if (ioctl(fd, USB_GET_DEVICEINFO, &udi) != 0) { | ||
60 | fido_log_debug("%s: get device info %s: %s", __func__, | ||
61 | path, strerror(errno)); | ||
62 | close(fd); | ||
63 | continue; | ||
64 | } | ||
65 | if ((rdesc = hid_get_report_desc(fd)) == NULL) { | ||
66 | fido_log_debug("%s: failed to get report descriptor: %s", | ||
67 | __func__, path); | ||
68 | close(fd); | ||
69 | continue; | ||
70 | } | ||
71 | if ((hdata = hid_start_parse(rdesc, | ||
72 | 1<<hid_collection, -1)) == NULL) { | ||
73 | fido_log_debug("%s: failed to parse report descriptor: %s", | ||
74 | __func__, path); | ||
75 | hid_dispose_report_desc(rdesc); | ||
76 | close(fd); | ||
77 | continue; | ||
78 | } | ||
79 | is_fido = 0; | ||
80 | for (is_fido = 0; !is_fido;) { | ||
81 | memset(&hitem, 0, sizeof(hitem)); | ||
82 | if (hid_get_item(hdata, &hitem) <= 0) | ||
83 | break; | ||
84 | if ((hitem._usage_page & 0xFFFF0000) == 0xf1d00000) | ||
85 | is_fido = 1; | ||
86 | } | ||
87 | hid_end_parse(hdata); | ||
88 | hid_dispose_report_desc(rdesc); | ||
89 | close(fd); | ||
90 | |||
91 | if (!is_fido) | ||
92 | continue; | ||
93 | |||
94 | fido_log_debug("%s: %s: bus = 0x%02x, addr = 0x%02x", | ||
95 | __func__, path, udi.udi_bus, udi.udi_addr); | ||
96 | fido_log_debug("%s: %s: vendor = \"%s\", product = \"%s\"", | ||
97 | __func__, path, udi.udi_vendor, udi.udi_product); | ||
98 | fido_log_debug("%s: %s: productNo = 0x%04x, vendorNo = 0x%04x, " | ||
99 | "releaseNo = 0x%04x", __func__, path, udi.udi_productNo, | ||
100 | udi.udi_vendorNo, udi.udi_releaseNo); | ||
101 | |||
102 | di = &devlist[*olen]; | ||
103 | memset(di, 0, sizeof(*di)); | ||
104 | if ((di->path = strdup(path)) == NULL || | ||
105 | (di->manufacturer = strdup(udi.udi_vendor)) == NULL || | ||
106 | (di->product = strdup(udi.udi_product)) == NULL) { | ||
107 | free(di->path); | ||
108 | free(di->manufacturer); | ||
109 | free(di->product); | ||
110 | explicit_bzero(di, sizeof(*di)); | ||
111 | return FIDO_ERR_INTERNAL; | ||
112 | } | ||
113 | di->vendor_id = udi.udi_vendorNo; | ||
114 | di->product_id = udi.udi_productNo; | ||
115 | (*olen)++; | ||
116 | } | ||
117 | |||
118 | return FIDO_OK; | ||
119 | } | ||
120 | |||
121 | /* | ||
122 | * Workaround for OpenBSD <=6.6-current (as of 201910) bug that loses | ||
123 | * sync of DATA0/DATA1 sequence bit across uhid open/close. | ||
124 | * Send pings until we get a response - early pings with incorrect | ||
125 | * sequence bits will be ignored as duplicate packets by the device. | ||
126 | */ | ||
127 | static int | ||
128 | terrible_ping_kludge(struct hid_openbsd *ctx) | ||
129 | { | ||
130 | u_char data[256]; | ||
131 | int i, n; | ||
132 | struct pollfd pfd; | ||
133 | |||
134 | if (sizeof(data) < ctx->report_out_len + 1) | ||
135 | return -1; | ||
136 | for (i = 0; i < 4; i++) { | ||
137 | memset(data, 0, sizeof(data)); | ||
138 | /* broadcast channel ID */ | ||
139 | data[1] = 0xff; | ||
140 | data[2] = 0xff; | ||
141 | data[3] = 0xff; | ||
142 | data[4] = 0xff; | ||
143 | /* Ping command */ | ||
144 | data[5] = 0x81; | ||
145 | /* One byte ping only, Vasili */ | ||
146 | data[6] = 0; | ||
147 | data[7] = 1; | ||
148 | fido_log_debug("%s: send ping %d", __func__, i); | ||
149 | if (fido_hid_write(ctx, data, ctx->report_out_len + 1) == -1) | ||
150 | return -1; | ||
151 | fido_log_debug("%s: wait reply", __func__); | ||
152 | memset(&pfd, 0, sizeof(pfd)); | ||
153 | pfd.fd = ctx->fd; | ||
154 | pfd.events = POLLIN; | ||
155 | if ((n = poll(&pfd, 1, 100)) == -1) { | ||
156 | fido_log_debug("%s: poll: %s", __func__, strerror(errno)); | ||
157 | return -1; | ||
158 | } else if (n == 0) { | ||
159 | fido_log_debug("%s: timed out", __func__); | ||
160 | continue; | ||
161 | } | ||
162 | if (fido_hid_read(ctx, data, ctx->report_out_len, 250) == -1) | ||
163 | return -1; | ||
164 | /* | ||
165 | * Ping isn't always supported on the broadcast channel, | ||
166 | * so we might get an error, but we don't care - we're | ||
167 | * synched now. | ||
168 | */ | ||
169 | fido_log_debug("%s: got reply", __func__); | ||
170 | fido_log_xxd(data, ctx->report_out_len); | ||
171 | return 0; | ||
172 | } | ||
173 | fido_log_debug("%s: no response", __func__); | ||
174 | return -1; | ||
175 | } | ||
176 | |||
177 | void * | ||
178 | fido_hid_open(const char *path) | ||
179 | { | ||
180 | struct hid_openbsd *ret = NULL; | ||
181 | report_desc_t rdesc = NULL; | ||
182 | int len, usb_report_id = 0; | ||
183 | |||
184 | if ((ret = calloc(1, sizeof(*ret))) == NULL || | ||
185 | (ret->fd = open(path, O_RDWR)) < 0) { | ||
186 | free(ret); | ||
187 | return (NULL); | ||
188 | } | ||
189 | if (ioctl(ret->fd, USB_GET_REPORT_ID, &usb_report_id) != 0) { | ||
190 | fido_log_debug("%s: failed to get report ID: %s", __func__, | ||
191 | strerror(errno)); | ||
192 | goto fail; | ||
193 | } | ||
194 | if ((rdesc = hid_get_report_desc(ret->fd)) == NULL) { | ||
195 | fido_log_debug("%s: failed to get report descriptor", __func__); | ||
196 | goto fail; | ||
197 | } | ||
198 | if ((len = hid_report_size(rdesc, hid_input, usb_report_id)) <= 0 || | ||
199 | (size_t)len > MAX_REPORT_LEN) { | ||
200 | fido_log_debug("%s: bad input report size %d", __func__, len); | ||
201 | goto fail; | ||
202 | } | ||
203 | ret->report_in_len = (size_t)len; | ||
204 | if ((len = hid_report_size(rdesc, hid_output, usb_report_id)) <= 0 || | ||
205 | (size_t)len > MAX_REPORT_LEN) { | ||
206 | fido_log_debug("%s: bad output report size %d", __func__, len); | ||
207 | fail: | ||
208 | hid_dispose_report_desc(rdesc); | ||
209 | close(ret->fd); | ||
210 | free(ret); | ||
211 | return NULL; | ||
212 | } | ||
213 | ret->report_out_len = (size_t)len; | ||
214 | hid_dispose_report_desc(rdesc); | ||
215 | fido_log_debug("%s: USB report ID %d, inlen = %zu outlen = %zu", | ||
216 | __func__, usb_report_id, ret->report_in_len, ret->report_out_len); | ||
217 | |||
218 | /* | ||
219 | * OpenBSD (as of 201910) has a bug that causes it to lose | ||
220 | * track of the DATA0/DATA1 sequence toggle across uhid device | ||
221 | * open and close. This is a terrible hack to work around it. | ||
222 | */ | ||
223 | if (terrible_ping_kludge(ret) != 0) { | ||
224 | fido_hid_close(ret); | ||
225 | return NULL; | ||
226 | } | ||
227 | |||
228 | return (ret); | ||
229 | } | ||
230 | |||
231 | void | ||
232 | fido_hid_close(void *handle) | ||
233 | { | ||
234 | struct hid_openbsd *ctx = (struct hid_openbsd *)handle; | ||
235 | |||
236 | close(ctx->fd); | ||
237 | free(ctx); | ||
238 | } | ||
239 | |||
240 | int | ||
241 | fido_hid_read(void *handle, unsigned char *buf, size_t len, int ms) | ||
242 | { | ||
243 | struct hid_openbsd *ctx = (struct hid_openbsd *)handle; | ||
244 | ssize_t r; | ||
245 | |||
246 | (void)ms; /* XXX */ | ||
247 | |||
248 | if (len != ctx->report_in_len) { | ||
249 | fido_log_debug("%s: invalid len: got %zu, want %zu", __func__, | ||
250 | len, ctx->report_in_len); | ||
251 | return (-1); | ||
252 | } | ||
253 | if ((r = read(ctx->fd, buf, len)) == -1 || (size_t)r != len) { | ||
254 | fido_log_debug("%s: read: %s", __func__, strerror(errno)); | ||
255 | return (-1); | ||
256 | } | ||
257 | return ((int)len); | ||
258 | } | ||
259 | |||
260 | int | ||
261 | fido_hid_write(void *handle, const unsigned char *buf, size_t len) | ||
262 | { | ||
263 | struct hid_openbsd *ctx = (struct hid_openbsd *)handle; | ||
264 | ssize_t r; | ||
265 | |||
266 | if (len != ctx->report_out_len + 1) { | ||
267 | fido_log_debug("%s: invalid len: got %zu, want %zu", __func__, | ||
268 | len, ctx->report_out_len); | ||
269 | return (-1); | ||
270 | } | ||
271 | if ((r = write(ctx->fd, buf + 1, len - 1)) == -1 || | ||
272 | (size_t)r != len - 1) { | ||
273 | fido_log_debug("%s: write: %s", __func__, strerror(errno)); | ||
274 | return (-1); | ||
275 | } | ||
276 | return ((int)len); | ||
277 | } | ||