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
29struct 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
38static int
39get_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
63static int
64get_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
85static int
86get_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
109static int
110get_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
133static int
134get_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;
158fail:
159	if (ok < 0) {
160		free(*manufacturer);
161		free(*product);
162		*manufacturer = NULL;
163		*product = NULL;
164	}
165
166	return (ok);
167}
168
169static char *
170get_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
195static bool
196is_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
220static int
221copy_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
241int
242fido_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;
300fail:
301	if (manager != NULL)
302		CFRelease(manager);
303	if (devset != NULL)
304		CFRelease(devset);
305
306	free(devs);
307
308	return (r);
309}
310
311static void
312report_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
337static void
338removal_callback(void *context, IOReturn result, void *sender)
339{
340	(void)context;
341	(void)result;
342	(void)sender;
343
344	CFRunLoopStop(CFRunLoopGetCurrent());
345}
346
347static int
348set_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
365static int
366disable_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
378static io_registry_entry_t
379get_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
395void *
396fido_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;
474fail:
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
494void
495fido_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
517int
518fido_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
526int
527fido_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
564int
565fido_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
583size_t
584fido_hid_report_in_len(void *handle)
585{
586	struct hid_osx *ctx = handle;
587
588	return (ctx->report_in_len);
589}
590
591size_t
592fido_hid_report_out_len(void *handle)
593{
594	struct hid_osx *ctx = handle;
595
596	return (ctx->report_out_len);
597}
598