// Build: // clang++ -std=c++17 -framework IOKit -framework CoreFoundation \ // fido_hid_example.mm -o fido_hid_example // // Run (may need to be run as a process with IOKit HID access, e.g. not // sandboxed): // ./fido_hid_example #include #include #include #include #include #include namespace { constexpr int kFidoUsagePage = 0xF1D0; constexpr int kFidoUsage = 0x01; constexpr size_t kReportSize = 64; } // namespace class FidoHidDevice { public: explicit FidoHidDevice(IOHIDDeviceRef device) : m_device(device), m_inputBuffer(kReportSize) { CFRetain(m_device); IOReturn res = IOHIDDeviceOpen(m_device, kIOHIDOptionsTypeNone); if (res != kIOReturnSuccess) { fprintf(stderr, "[ctx=%p] IOHIDDeviceOpen failed: 0x%08x\n", this, res); return; } // Re-opening a previously-closed, still-retained IOHIDDeviceRef does // not re-arm its run loop scheduling implicitly - it must be // scheduled again on every open, or input report callbacks never fire. IOHIDDeviceScheduleWithRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOHIDDeviceRegisterInputReportCallback(m_device, m_inputBuffer.data(), static_cast(m_inputBuffer.size()), &FidoHidDevice::onInputReport, this); m_open = true; printf("[ctx=%p] opened + subscribed\n", this); } ~FidoHidDevice() { if (m_open) { // Unregister before closing so no callback can land on a device // that's mid-teardown. IOHIDDeviceRegisterInputReportCallback(m_device, m_inputBuffer.data(), static_cast(m_inputBuffer.size()), nullptr, nullptr); IOHIDDeviceUnscheduleFromRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOHIDDeviceClose(m_device, kIOHIDOptionsTypeNone); printf("[ctx=%p] unsubscribed + closed\n", this); } CFRelease(m_device); } bool isOpen() const { return m_open; } IOReturn writeReport(const uint8_t* data, size_t length) { return IOHIDDeviceSetReport(m_device, kIOHIDReportTypeOutput, /*reportID=*/0, data, static_cast(length)); } private: // context is the FidoHidDevice instance that registered this callback - // i.e. the value passed as the last argument to // IOHIDDeviceRegisterInputReportCallback in the constructor. static void onInputReport(void* context, IOReturn result, void* /*sender*/, IOHIDReportType /*type*/, uint32_t reportID, uint8_t* report, CFIndex length) { printf("[ctx=%p] input report result=0x%08x reportID=%u length=%ld:", context, result, reportID, static_cast(length)); for (CFIndex i = 0; i < length; ++i) { printf(" %02x", report[i]); } printf("\n"); } IOHIDDeviceRef m_device; std::vector m_inputBuffer; bool m_open = false; }; // Matches on usage page/usage and returns the first FIDO device found, // retained for the caller. static IOHIDDeviceRef findFidoDevice(IOHIDManagerRef manager) { CFMutableDictionaryRef matching = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFNumberRef usagePage = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &kFidoUsagePage); CFNumberRef usage = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &kFidoUsage); CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsagePageKey), usagePage); CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsageKey), usage); CFRelease(usagePage); CFRelease(usage); IOHIDManagerSetDeviceMatching(manager, matching); CFRelease(matching); auto deviceSet = static_cast(IOHIDManagerCopyDevices(manager)); if (!deviceSet) { return nullptr; } IOHIDDeviceRef found = nullptr; CFIndex count = CFSetGetCount(deviceSet); if (count > 0) { std::vector devices(static_cast(count)); CFSetGetValues(deviceSet, devices.data()); found = static_cast(const_cast(devices[0])); CFRetain(found); // outlive the CFRelease(deviceSet) below } CFRelease(deviceSet); return found; } int main() { IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOReturn openRes = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); if (openRes != kIOReturnSuccess) { fprintf(stderr, "IOHIDManagerOpen failed: 0x%08x\n", openRes); CFRelease(manager); return 1; } IOHIDDeviceRef fidoDevice = findFidoDevice(manager); if (!fidoDevice) { fprintf(stderr, "No FIDO device (usage page 0x%04x) found.\n", kFidoUsagePage); IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); CFRelease(manager); return 1; } // 1. Open, subscribe (context = &device, this instance's own address), close. { FidoHidDevice device(fidoDevice); if (!device.isOpen()) { fprintf(stderr, "First open failed.\n"); CFRelease(fidoDevice); IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); CFRelease(manager); return 1; } // Let any already-pending report land before we tear this down. CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.2, false); } // ~FidoHidDevice: unsubscribe + close happens here. // 2. Major delay before next "subscription" CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10.0, false); // 3. Re-open as a new instance (a different context pointer), subscribe, // write, wait for the reply. { FidoHidDevice device(fidoDevice); if (!device.isOpen()) { fprintf(stderr, "Second open failed.\n"); CFRelease(fidoDevice); IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); CFRelease(manager); return 1; } // Minimal CTAPHID_INIT request: broadcast CID, INIT command, an // 8-byte nonce. Real clients should use a random nonce and should // match the reply's nonce/CID before trusting it. std::vector report(kReportSize, 0); report[0] = 0xff; // CID (broadcast) report[1] = 0xff; report[2] = 0xff; report[3] = 0xff; report[4] = 0x86; // CTAPHID_INIT = 0x06 | 0x80 (init packet bit) report[5] = 0x00; // BCNT high byte report[6] = 0x08; // BCNT low byte: 8-byte nonce payload for (int i = 0; i < 8; ++i) { report[7 + i] = static_cast(i); } IOReturn writeRes = device.writeReport(report.data(), report.size()); if (writeRes != kIOReturnSuccess) { fprintf(stderr, "writeReport failed: 0x%08x\n", writeRes); } else { printf("Wrote CTAPHID_INIT request, waiting for reply...\n"); } // Pump the run loop so onInputReport can fire with the device's reply. CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2.0, false); } // ~FidoHidDevice: unsubscribe + close happens here. CFRelease(fidoDevice); IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); CFRelease(manager); return 0; }