JustMe
JustMe

Reputation: 63

Keystrokes are not blocked when using kIOHIDOptionsTypeSeizeDevice and are still passed to the OS

My goal is to block keystrokes from reaching the OS using IOHID (Can't use CGEvent for other reasons). According to the docs of kIOHIDOptionsTypeSeizeDevice:

Used to open exclusive communication with the device. This will prevent the system and other clients from receiving events from the device.

#import "TestKeys.h"
#import <IOKit/hid/IOHIDManager.h>
#import <IOKit/hid/IOHIDUsageTables.h>

@implementation TestKeys

#define KEYS 2

static void Handle_InputCallback(void *inContext, IOReturn inResult, void *inSender, IOHIDValueRef value)
{
    IOHIDElementRef elem = IOHIDValueGetElement(value);

    uint16_t scancode = IOHIDElementGetUsage(elem);
    
    if (scancode < 4 || scancode > 231) {
        return;
    }
    
    NSLog(@"Key event received: %d", scancode);
}

static void Handle_DeviceMatchingCallback(void * inContext, IOReturn inResult, void * inSender, IOHIDDeviceRef inIOHIDDeviceRef)
{
    NSLog(@"Connected");
}

static void Handle_RemovalCallback(void * inContext, IOReturn inResult, void * inSender, IOHIDDeviceRef inIOHIDDeviceRef)
{
    NSLog(@"Removed");
}

-(void)start
{
    IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone);
    
    if (CFGetTypeID(manager) != IOHIDManagerGetTypeID()) {
        exit(1);
    }

    int usagePage = kHIDPage_GenericDesktop;
    int usage = kHIDUsage_GD_Keyboard;
    
    CFStringRef keys[KEYS] = {
        CFSTR(kIOHIDDeviceUsagePageKey),
        CFSTR(kIOHIDDeviceUsageKey),
    };
    
    CFNumberRef values[KEYS] = {
        CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usagePage),
        CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usage),
    };
    
    CFDictionaryRef matchingDict = CFDictionaryCreate(kCFAllocatorDefault,
                                                      (const void **) keys, (const void **) values, KEYS,
                                                      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

    for (int i=0; i<KEYS; i++) {
        CFRelease(keys[i]);
        CFRelease(values[i]);
    }
    
    IOHIDManagerSetDeviceMatching(manager, matchingDict);
    CFRelease(matchingDict);
    
    IOHIDManagerRegisterDeviceMatchingCallback(manager, Handle_DeviceMatchingCallback, NULL);
    IOHIDManagerRegisterDeviceRemovalCallback(manager, Handle_RemovalCallback, NULL);
    IOHIDManagerRegisterInputValueCallback(manager, Handle_InputCallback, NULL);
    
    IOHIDManagerOpen(manager, kIOHIDOptionsTypeSeizeDevice);

    IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
@end

This code runs and manages to see and print all keystrokes globally from the OS but it seems like kIOHIDOptionsTypeSeizeDevice is being ignored as keystrokes are still being passed on to macOS.

Edit: Adding IOReturn result = to the code exposed the error -536870207 which translates to kIOReturnNotPrivileged. I've then changed the Xcode scheme to root and was able to block the keyboard keys.

Which leads me to the next question, how can I add this code to a Developer ID app that obviously doesn't run with root privileges?

Upvotes: 2

Views: 342

Answers (1)

pmdj
pmdj

Reputation: 23438

Glad we tracked down your HID issue to a permissions problem in the comments.

To run code as root in production/deployment, you'll need to set up a separate tool as a Launch Daemon, and configure it to start up on demand when your main app sends it an IPC (typically XPC) message.

You have 2 main options for getting the launch daemon set up in the first place:

  • Embedding the daemon binary in your .app, and calling SMJobBless from your app's code to install it in the system. This will request entering the admin password from the user.
  • An installer .pkg that places your daemon binary somewhere fixed (e.g. below /Library/Application Support/YourAppName/) and places the corresponding launchd plist in /Library/LaunchDaemons.

Note that you need to be quite careful to avoid opening gaping security holes when setting up a launch daemon. This series of articles is a good in-depth guide of what to do and what to avoid.

Note also that SMJobBless is not permitted on the App Store, so this type of functionality simply isn't possible for apps distributed there.

Upvotes: 3

Related Questions