1 /*
2 * Copyright (c) 2019-2022 Yubico AB. 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 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8 #include <sys/types.h>
9
10 #include <errno.h>
11 #include <fcntl.h>
12 #include <signal.h>
13 #include <unistd.h>
14
15 #include <Availability.h>
16 #include <CoreFoundation/CoreFoundation.h>
17 #include <IOKit/IOKitLib.h>
18 #include <IOKit/hid/IOHIDKeys.h>
19 #include <IOKit/hid/IOHIDManager.h>
20
21 #include "fido.h"
22
23 #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
24 #define kIOMainPortDefault kIOMasterPortDefault
25 #endif
26
27 #define IOREG "ioreg://"
28
29 struct hid_osx {
30 IOHIDDeviceRef ref;
31 CFStringRef loop_id;
32 int report_pipe[2];
33 size_t report_in_len;
34 size_t report_out_len;
35 unsigned char report[CTAP_MAX_REPORT_LEN];
36 };
37
38 static int
get_int32(IOHIDDeviceRef dev,CFStringRef key,int32_t * v)39 get_int32(IOHIDDeviceRef dev, CFStringRef key, int32_t *v)
40 {
41 CFTypeRef ref;
42
43 if ((ref = IOHIDDeviceGetProperty(dev, key)) == NULL ||
44 CFGetTypeID(ref) != CFNumberGetTypeID()) {
45 fido_log_debug("%s: IOHIDDeviceGetProperty", __func__);
46 return (-1);
47 }
48
49 if (CFNumberGetType(ref) != kCFNumberSInt32Type &&
50 CFNumberGetType(ref) != kCFNumberSInt64Type) {
51 fido_log_debug("%s: CFNumberGetType", __func__);
52 return (-1);
53 }
54
55 if (CFNumberGetValue(ref, kCFNumberSInt32Type, v) == false) {
56 fido_log_debug("%s: CFNumberGetValue", __func__);
57 return (-1);
58 }
59
60 return (0);
61 }
62
63 static int
get_utf8(IOHIDDeviceRef dev,CFStringRef key,void * buf,size_t len)64 get_utf8(IOHIDDeviceRef dev, CFStringRef key, void *buf, size_t len)
65 {
66 CFTypeRef ref;
67
68 memset(buf, 0, len);
69
70 if ((ref = IOHIDDeviceGetProperty(dev, key)) == NULL ||
71 CFGetTypeID(ref) != CFStringGetTypeID()) {
72 fido_log_debug("%s: IOHIDDeviceGetProperty", __func__);
73 return (-1);
74 }
75
76 if (CFStringGetCString(ref, buf, (long)len,
77 kCFStringEncodingUTF8) == false) {
78 fido_log_debug("%s: CFStringGetCString", __func__);
79 return (-1);
80 }
81
82 return (0);
83 }
84
85 static int
get_report_len(IOHIDDeviceRef dev,int dir,size_t * report_len)86 get_report_len(IOHIDDeviceRef dev, int dir, size_t *report_len)
87 {
88 CFStringRef key;
89 int32_t v;
90
91 if (dir == 0)
92 key = CFSTR(kIOHIDMaxInputReportSizeKey);
93 else
94 key = CFSTR(kIOHIDMaxOutputReportSizeKey);
95
96 if (get_int32(dev, key, &v) < 0) {
97 fido_log_debug("%s: get_int32/%d", __func__, dir);
98 return (-1);
99 }
100
101 if ((*report_len = (size_t)v) > CTAP_MAX_REPORT_LEN) {
102 fido_log_debug("%s: report_len=%zu", __func__, *report_len);
103 return (-1);
104 }
105
106 return (0);
107 }
108
109 static int
get_id(IOHIDDeviceRef dev,int16_t * vendor_id,int16_t * product_id)110 get_id(IOHIDDeviceRef dev, int16_t *vendor_id, int16_t *product_id)
111 {
112 int32_t vendor;
113 int32_t product;
114
115 if (get_int32(dev, CFSTR(kIOHIDVendorIDKey), &vendor) < 0 ||
116 vendor > UINT16_MAX) {
117 fido_log_debug("%s: get_int32 vendor", __func__);
118 return (-1);
119 }
120
121 if (get_int32(dev, CFSTR(kIOHIDProductIDKey), &product) < 0 ||
122 product > UINT16_MAX) {
123 fido_log_debug("%s: get_int32 product", __func__);
124 return (-1);
125 }
126
127 *vendor_id = (int16_t)vendor;
128 *product_id = (int16_t)product;
129
130 return (0);
131 }
132
133 static int
get_str(IOHIDDeviceRef dev,char ** manufacturer,char ** product)134 get_str(IOHIDDeviceRef dev, char **manufacturer, char **product)
135 {
136 char buf[512];
137 int ok = -1;
138
139 *manufacturer = NULL;
140 *product = NULL;
141
142 if (get_utf8(dev, CFSTR(kIOHIDManufacturerKey), buf, sizeof(buf)) < 0)
143 *manufacturer = strdup("");
144 else
145 *manufacturer = strdup(buf);
146
147 if (get_utf8(dev, CFSTR(kIOHIDProductKey), buf, sizeof(buf)) < 0)
148 *product = strdup("");
149 else
150 *product = strdup(buf);
151
152 if (*manufacturer == NULL || *product == NULL) {
153 fido_log_debug("%s: strdup", __func__);
154 goto fail;
155 }
156
157 ok = 0;
158 fail:
159 if (ok < 0) {
160 free(*manufacturer);
161 free(*product);
162 *manufacturer = NULL;
163 *product = NULL;
164 }
165
166 return (ok);
167 }
168
169 static char *
get_path(IOHIDDeviceRef dev)170 get_path(IOHIDDeviceRef dev)
171 {
172 io_service_t s;
173 uint64_t id;
174 char *path;
175
176 if ((s = IOHIDDeviceGetService(dev)) == MACH_PORT_NULL) {
177 fido_log_debug("%s: IOHIDDeviceGetService", __func__);
178 return (NULL);
179 }
180
181 if (IORegistryEntryGetRegistryEntryID(s, &id) != KERN_SUCCESS) {
182 fido_log_debug("%s: IORegistryEntryGetRegistryEntryID",
183 __func__);
184 return (NULL);
185 }
186
187 if (asprintf(&path, "%s%llu", IOREG, (unsigned long long)id) == -1) {
188 fido_log_error(errno, "%s: asprintf", __func__);
189 return (NULL);
190 }
191
192 return (path);
193 }
194
195 static bool
is_fido(IOHIDDeviceRef dev)196 is_fido(IOHIDDeviceRef dev)
197 {
198 char buf[32];
199 uint32_t usage_page;
200
201 if (get_int32(dev, CFSTR(kIOHIDPrimaryUsagePageKey),
202 (int32_t *)&usage_page) < 0 || usage_page != 0xf1d0)
203 return (false);
204
205 if (get_utf8(dev, CFSTR(kIOHIDTransportKey), buf, sizeof(buf)) < 0) {
206 fido_log_debug("%s: get_utf8 transport", __func__);
207 return (false);
208 }
209
210 #ifndef FIDO_HID_ANY
211 if (strcasecmp(buf, "usb") != 0) {
212 fido_log_debug("%s: transport", __func__);
213 return (false);
214 }
215 #endif
216
217 return (true);
218 }
219
220 static int
copy_info(fido_dev_info_t * di,IOHIDDeviceRef dev)221 copy_info(fido_dev_info_t *di, IOHIDDeviceRef dev)
222 {
223 memset(di, 0, sizeof(*di));
224
225 if (is_fido(dev) == false)
226 return (-1);
227
228 if (get_id(dev, &di->vendor_id, &di->product_id) < 0 ||
229 get_str(dev, &di->manufacturer, &di->product) < 0 ||
230 (di->path = get_path(dev)) == NULL) {
231 free(di->path);
232 free(di->manufacturer);
233 free(di->product);
234 explicit_bzero(di, sizeof(*di));
235 return (-1);
236 }
237
238 return (0);
239 }
240
241 int
fido_hid_manifest(fido_dev_info_t * devlist,size_t ilen,size_t * olen)242 fido_hid_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen)
243 {
244 IOHIDManagerRef manager = NULL;
245 CFSetRef devset = NULL;
246 size_t devcnt;
247 CFIndex n;
248 IOHIDDeviceRef *devs = NULL;
249 int r = FIDO_ERR_INTERNAL;
250
251 *olen = 0;
252
253 if (ilen == 0)
254 return (FIDO_OK); /* nothing to do */
255
256 if (devlist == NULL)
257 return (FIDO_ERR_INVALID_ARGUMENT);
258
259 if ((manager = IOHIDManagerCreate(kCFAllocatorDefault,
260 kIOHIDManagerOptionNone)) == NULL) {
261 fido_log_debug("%s: IOHIDManagerCreate", __func__);
262 goto fail;
263 }
264
265 IOHIDManagerSetDeviceMatching(manager, NULL);
266
267 if ((devset = IOHIDManagerCopyDevices(manager)) == NULL) {
268 fido_log_debug("%s: IOHIDManagerCopyDevices", __func__);
269 goto fail;
270 }
271
272 if ((n = CFSetGetCount(devset)) < 0) {
273 fido_log_debug("%s: CFSetGetCount", __func__);
274 goto fail;
275 }
276
277 devcnt = (size_t)n;
278
279 if ((devs = calloc(devcnt, sizeof(*devs))) == NULL) {
280 fido_log_debug("%s: calloc", __func__);
281 goto fail;
282 }
283
284 CFSetGetValues(devset, (void *)devs);
285
286 for (size_t i = 0; i < devcnt; i++) {
287 if (copy_info(&devlist[*olen], devs[i]) == 0) {
288 devlist[*olen].io = (fido_dev_io_t) {
289 fido_hid_open,
290 fido_hid_close,
291 fido_hid_read,
292 fido_hid_write,
293 };
294 if (++(*olen) == ilen)
295 break;
296 }
297 }
298
299 r = FIDO_OK;
300 fail:
301 if (manager != NULL)
302 CFRelease(manager);
303 if (devset != NULL)
304 CFRelease(devset);
305
306 free(devs);
307
308 return (r);
309 }
310
311 static void
report_callback(void * context,IOReturn result,void * dev,IOHIDReportType type,uint32_t id,uint8_t * ptr,CFIndex len)312 report_callback(void *context, IOReturn result, void *dev, IOHIDReportType type,
313 uint32_t id, uint8_t *ptr, CFIndex len)
314 {
315 struct hid_osx *ctx = context;
316 ssize_t r;
317
318 (void)dev;
319
320 if (result != kIOReturnSuccess || type != kIOHIDReportTypeInput ||
321 id != 0 || len < 0 || (size_t)len != ctx->report_in_len) {
322 fido_log_debug("%s: io error", __func__);
323 return;
324 }
325
326 if ((r = write(ctx->report_pipe[1], ptr, (size_t)len)) == -1) {
327 fido_log_error(errno, "%s: write", __func__);
328 return;
329 }
330
331 if (r < 0 || (size_t)r != (size_t)len) {
332 fido_log_debug("%s: %zd != %zu", __func__, r, (size_t)len);
333 return;
334 }
335 }
336
337 static void
removal_callback(void * context,IOReturn result,void * sender)338 removal_callback(void *context, IOReturn result, void *sender)
339 {
340 (void)context;
341 (void)result;
342 (void)sender;
343
344 CFRunLoopStop(CFRunLoopGetCurrent());
345 }
346
347 static int
set_nonblock(int fd)348 set_nonblock(int fd)
349 {
350 int flags;
351
352 if ((flags = fcntl(fd, F_GETFL)) == -1) {
353 fido_log_error(errno, "%s: fcntl F_GETFL", __func__);
354 return (-1);
355 }
356
357 if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
358 fido_log_error(errno, "%s: fcntl F_SETFL", __func__);
359 return (-1);
360 }
361
362 return (0);
363 }
364
365 static int
disable_sigpipe(int fd)366 disable_sigpipe(int fd)
367 {
368 int disabled = 1;
369
370 if (fcntl(fd, F_SETNOSIGPIPE, &disabled) == -1) {
371 fido_log_error(errno, "%s: fcntl F_SETNOSIGPIPE", __func__);
372 return (-1);
373 }
374
375 return (0);
376 }
377
378 static io_registry_entry_t
get_ioreg_entry(const char * path)379 get_ioreg_entry(const char *path)
380 {
381 uint64_t id;
382
383 if (strncmp(path, IOREG, strlen(IOREG)) != 0)
384 return (IORegistryEntryFromPath(kIOMainPortDefault, path));
385
386 if (fido_to_uint64(path + strlen(IOREG), 10, &id) == -1) {
387 fido_log_debug("%s: fido_to_uint64", __func__);
388 return (MACH_PORT_NULL);
389 }
390
391 return (IOServiceGetMatchingService(kIOMainPortDefault,
392 IORegistryEntryIDMatching(id)));
393 }
394
395 void *
fido_hid_open(const char * path)396 fido_hid_open(const char *path)
397 {
398 struct hid_osx *ctx;
399 io_registry_entry_t entry = MACH_PORT_NULL;
400 char loop_id[32];
401 int ok = -1;
402 int r;
403
404 if ((ctx = calloc(1, sizeof(*ctx))) == NULL) {
405 fido_log_debug("%s: calloc", __func__);
406 goto fail;
407 }
408
409 ctx->report_pipe[0] = -1;
410 ctx->report_pipe[1] = -1;
411
412 if (pipe(ctx->report_pipe) == -1) {
413 fido_log_error(errno, "%s: pipe", __func__);
414 goto fail;
415 }
416
417 if (set_nonblock(ctx->report_pipe[0]) < 0 ||
418 set_nonblock(ctx->report_pipe[1]) < 0) {
419 fido_log_debug("%s: set_nonblock", __func__);
420 goto fail;
421 }
422
423 if (disable_sigpipe(ctx->report_pipe[1]) < 0) {
424 fido_log_debug("%s: disable_sigpipe", __func__);
425 goto fail;
426 }
427
428 if ((entry = get_ioreg_entry(path)) == MACH_PORT_NULL) {
429 fido_log_debug("%s: get_ioreg_entry: %s", __func__, path);
430 goto fail;
431 }
432
433 if ((ctx->ref = IOHIDDeviceCreate(kCFAllocatorDefault,
434 entry)) == NULL) {
435 fido_log_debug("%s: IOHIDDeviceCreate", __func__);
436 goto fail;
437 }
438
439 if (get_report_len(ctx->ref, 0, &ctx->report_in_len) < 0 ||
440 get_report_len(ctx->ref, 1, &ctx->report_out_len) < 0) {
441 fido_log_debug("%s: get_report_len", __func__);
442 goto fail;
443 }
444
445 if (ctx->report_in_len > sizeof(ctx->report)) {
446 fido_log_debug("%s: report_in_len=%zu", __func__,
447 ctx->report_in_len);
448 goto fail;
449 }
450
451 if (IOHIDDeviceOpen(ctx->ref,
452 kIOHIDOptionsTypeSeizeDevice) != kIOReturnSuccess) {
453 fido_log_debug("%s: IOHIDDeviceOpen", __func__);
454 goto fail;
455 }
456
457 if ((r = snprintf(loop_id, sizeof(loop_id), "fido2-%p",
458 (void *)ctx->ref)) < 0 || (size_t)r >= sizeof(loop_id)) {
459 fido_log_debug("%s: snprintf", __func__);
460 goto fail;
461 }
462
463 if ((ctx->loop_id = CFStringCreateWithCString(NULL, loop_id,
464 kCFStringEncodingASCII)) == NULL) {
465 fido_log_debug("%s: CFStringCreateWithCString", __func__);
466 goto fail;
467 }
468
469 IOHIDDeviceRegisterInputReportCallback(ctx->ref, ctx->report,
470 (long)ctx->report_in_len, &report_callback, ctx);
471 IOHIDDeviceRegisterRemovalCallback(ctx->ref, &removal_callback, ctx);
472
473 ok = 0;
474 fail:
475 if (entry != MACH_PORT_NULL)
476 IOObjectRelease(entry);
477
478 if (ok < 0 && ctx != NULL) {
479 if (ctx->ref != NULL)
480 CFRelease(ctx->ref);
481 if (ctx->loop_id != NULL)
482 CFRelease(ctx->loop_id);
483 if (ctx->report_pipe[0] != -1)
484 close(ctx->report_pipe[0]);
485 if (ctx->report_pipe[1] != -1)
486 close(ctx->report_pipe[1]);
487 free(ctx);
488 ctx = NULL;
489 }
490
491 return (ctx);
492 }
493
494 void
fido_hid_close(void * handle)495 fido_hid_close(void *handle)
496 {
497 struct hid_osx *ctx = handle;
498
499 IOHIDDeviceRegisterInputReportCallback(ctx->ref, ctx->report,
500 (long)ctx->report_in_len, NULL, ctx);
501 IOHIDDeviceRegisterRemovalCallback(ctx->ref, NULL, ctx);
502
503 if (IOHIDDeviceClose(ctx->ref,
504 kIOHIDOptionsTypeSeizeDevice) != kIOReturnSuccess)
505 fido_log_debug("%s: IOHIDDeviceClose", __func__);
506
507 CFRelease(ctx->ref);
508 CFRelease(ctx->loop_id);
509
510 explicit_bzero(ctx->report, sizeof(ctx->report));
511 close(ctx->report_pipe[0]);
512 close(ctx->report_pipe[1]);
513
514 free(ctx);
515 }
516
517 int
fido_hid_set_sigmask(void * handle,const fido_sigset_t * sigmask)518 fido_hid_set_sigmask(void *handle, const fido_sigset_t *sigmask)
519 {
520 (void)handle;
521 (void)sigmask;
522
523 return (FIDO_ERR_INTERNAL);
524 }
525
526 int
fido_hid_read(void * handle,unsigned char * buf,size_t len,int ms)527 fido_hid_read(void *handle, unsigned char *buf, size_t len, int ms)
528 {
529 struct hid_osx *ctx = handle;
530 ssize_t r;
531
532 explicit_bzero(buf, len);
533 explicit_bzero(ctx->report, sizeof(ctx->report));
534
535 if (len != ctx->report_in_len || len > sizeof(ctx->report)) {
536 fido_log_debug("%s: len %zu", __func__, len);
537 return (-1);
538 }
539
540 IOHIDDeviceScheduleWithRunLoop(ctx->ref, CFRunLoopGetCurrent(),
541 ctx->loop_id);
542
543 if (ms == -1)
544 ms = 5000; /* wait 5 seconds by default */
545
546 CFRunLoopRunInMode(ctx->loop_id, (double)ms/1000.0, true);
547
548 IOHIDDeviceUnscheduleFromRunLoop(ctx->ref, CFRunLoopGetCurrent(),
549 ctx->loop_id);
550
551 if ((r = read(ctx->report_pipe[0], buf, len)) == -1) {
552 fido_log_error(errno, "%s: read", __func__);
553 return (-1);
554 }
555
556 if (r < 0 || (size_t)r != len) {
557 fido_log_debug("%s: %zd != %zu", __func__, r, len);
558 return (-1);
559 }
560
561 return ((int)len);
562 }
563
564 int
fido_hid_write(void * handle,const unsigned char * buf,size_t len)565 fido_hid_write(void *handle, const unsigned char *buf, size_t len)
566 {
567 struct hid_osx *ctx = handle;
568
569 if (len != ctx->report_out_len + 1 || len > LONG_MAX) {
570 fido_log_debug("%s: len %zu", __func__, len);
571 return (-1);
572 }
573
574 if (IOHIDDeviceSetReport(ctx->ref, kIOHIDReportTypeOutput, 0, buf + 1,
575 (long)(len - 1)) != kIOReturnSuccess) {
576 fido_log_debug("%s: IOHIDDeviceSetReport", __func__);
577 return (-1);
578 }
579
580 return ((int)len);
581 }
582
583 size_t
fido_hid_report_in_len(void * handle)584 fido_hid_report_in_len(void *handle)
585 {
586 struct hid_osx *ctx = handle;
587
588 return (ctx->report_in_len);
589 }
590
591 size_t
fido_hid_report_out_len(void * handle)592 fido_hid_report_out_len(void *handle)
593 {
594 struct hid_osx *ctx = handle;
595
596 return (ctx->report_out_len);
597 }
598