├── .gitignore ├── Makefile ├── com.podtynnyi.yubikeylockd.plist ├── README.md ├── License.txt ├── yubikeylockd.rb └── yubikeylockd.c /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | yubikeylockd 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=clang 2 | 3 | FRAMEWORKS:= -framework IOKit -framework CoreFoundation 4 | 5 | SOURCE=yubikeylockd.c 6 | 7 | CFLAGS=-Wall -Werror -O2 $(SOURCE) 8 | LDFLAGS=$(LIBRARIES) $(FRAMEWORKS) 9 | OUT=-o yubikeylockd 10 | 11 | all: yubikeylockd 12 | 13 | clean: 14 | rm -rf yubikeylockd 15 | 16 | yubikeylockd: 17 | $(CC) $(CFLAGS) $(LDFLAGS) $(OUT) 18 | -------------------------------------------------------------------------------- /com.podtynnyi.yubikeylockd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.podtynnyi.com.yubikeylockd 7 | ProgramArguments 8 | 9 | /usr/local/bin/yubikeylockd 10 | 11 | LaunchEvents 12 | 13 | com.apple.iokit.matching 14 | 15 | com.apple.device-attach 16 | 17 | idProduct 18 | * 19 | idVendor 20 | 4176 21 | IOProviderClass 22 | IOUSBDevice 23 | IOMatchLaunchStream 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yubikeylockd 2 | 3 | Simple daemon for locking and unlocking macOS with Yubikey. 4 | 5 | ## Install 6 | 7 | Via Homebrew formula: 8 | 9 | ``` 10 | brew tap shtirlic/yubikeylockd https://github.com/shtirlic/yubikeylockd 11 | brew install shtirlic/yubikeylockd/yubikeylockd 12 | ``` 13 | 14 | ## Additional requirements 15 | * [YubiKey using the native smart card (PIV) mode](https://www.yubico.com/why-yubico/for-businesses/computer-login/mac-os-login/) 16 | * Require password *immediately* after sleep or screen saver begins 17 | ![](http://i.imgur.com/URXUukP.png) 18 | 19 | ## How it works 20 | 21 | When you attach Yubikey for the first time `launchctl` will run `yubikeylockd` daemon 22 | that will simply monitor the state of the Yubikey USB devices. 23 | Daemon based on the sample provided by Apple for IOKit development. 24 | 25 | It does two things: 26 | * when device is attached it makes activity via 27 | ```IOPMAssertionDeclareUserActivity``` call to turn screen on 28 | * after device is detached it uses ```IORequestIdle``` to put display to sleep and (if you configured it) also locks the OS X 29 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2018 Serg Podtynnyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /yubikeylockd.rb: -------------------------------------------------------------------------------- 1 | class Yubikeylockd < Formula 2 | desc 'Simple daemon for locking and unlocking macOS with Yubikey.' 3 | homepage 'https://github.com/shtirlic/yubikeylockd' 4 | url 'https://github.com/shtirlic/yubikeylockd/archive/v1.1.zip' 5 | head 'https://github.com/shtirlic/yubikeylockd.git' 6 | sha256 '44ba63cb286c29fa653d2307f053162bb600a7138e6f6766d40144a504a376b7' 7 | 8 | def install 9 | # ENV.deparallelize # if your formula fails when building in parallel 10 | system 'make', 'yubikeylockd' 11 | # system "install" # if this fails, try separate make/make install steps 12 | bin.install 'yubikeylockd' 13 | end 14 | 15 | plist_options startup: true 16 | 17 | def plist 18 | <<-PLIST 19 | 20 | 21 | 22 | 23 | Label 24 | #{plist_name} 25 | ProgramArguments 26 | 27 | #{bin}/yubikeylockd 28 | 29 | LaunchEvents 30 | 31 | com.apple.iokit.matching 32 | 33 | com.apple.device-attach 34 | 35 | idProduct 36 | * 37 | idVendor 38 | 4176 39 | IOProviderClass 40 | IOUSBDevice 41 | IOMatchLaunchStream 42 | 43 | 44 | 45 | 46 | 47 | 48 | PLIST 49 | end 50 | 51 | test do 52 | # `test do` will create, run in and delete a temporary directory. 53 | # 54 | # This test will fail and we won't accept that! It's enough to just replace 55 | # "false" with the main program this formula installs, but it'd be nice if you 56 | # were more thorough. Run the test with `brew test yubikeylockd`. Options passed 57 | # to `brew install` such as `--HEAD` also need to be provided to `brew test`. 58 | # 59 | # The installed folder is not in the path, so use the entire path to any 60 | # executables being tested: `system "#{bin}/program", "do", "something"`. 61 | system 'true' 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /yubikeylockd.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | // This is the USB vendor ID for Yubico 13 | #define kMyVendorID 0x1050 14 | 15 | 16 | typedef struct MyPrivateData { 17 | io_object_t notification; 18 | IOUSBDeviceInterface * *deviceInterface; 19 | CFStringRef deviceName; 20 | UInt32 locationID; 21 | } MyPrivateData; 22 | 23 | //================================================================================================ 24 | // Globals 25 | //================================================================================================ 26 | // 27 | static IONotificationPortRef gNotifyPort; 28 | static io_iterator_t gAddedIter; 29 | static CFRunLoopRef gRunLoop; 30 | 31 | void DeviceNotification( void * refCon, 32 | io_service_t service, 33 | natural_t messageType, 34 | void * messageArgument ) 35 | { 36 | if (messageType == kIOMessageServiceIsTerminated) 37 | { 38 | printf("Device 0x%08x removed.\n", service); 39 | 40 | // Run lock via idle 41 | // 42 | printf("Yubikey removed. Lock the screen.\n"); 43 | 44 | // Lock the keychain too 45 | system("/usr/bin/security lock-keychain"); 46 | 47 | io_registry_entry_t reg = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); 48 | if (reg) { 49 | IORegistryEntrySetCFProperty(reg, CFSTR("IORequestIdle"), kCFBooleanTrue); 50 | IOObjectRelease(reg); 51 | } 52 | 53 | } 54 | } 55 | 56 | void DeviceAdded(void *refCon, io_iterator_t iterator) 57 | { 58 | kern_return_t kr; 59 | io_service_t usbDevice; 60 | IOCFPlugInInterface **plugInInterface=NULL; 61 | SInt32 score; 62 | HRESULT res; 63 | 64 | while ( (usbDevice = IOIteratorNext(iterator)) ) 65 | { 66 | io_name_t deviceName; 67 | CFStringRef deviceNameAsCFString; 68 | MyPrivateData *privateDataRef = NULL; 69 | UInt32 locationID; 70 | 71 | printf("Device 0x%08x added.\n", usbDevice); 72 | 73 | // Make activity and turn screen on 74 | printf("Wake up on Yubikey insertion.\n"); 75 | IOPMAssertionID assertionID; 76 | IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); 77 | 78 | // Add some app-specific information about this device. 79 | // Create a buffer to hold the data. 80 | 81 | privateDataRef = malloc(sizeof(MyPrivateData)); 82 | bzero( privateDataRef, sizeof(MyPrivateData)); 83 | 84 | // In this sample we'll just use the service's name. 85 | // 86 | kr = IORegistryEntryGetName(usbDevice, deviceName); 87 | if (KERN_SUCCESS != kr) 88 | { 89 | deviceName[0] = '\0'; 90 | } 91 | 92 | deviceNameAsCFString = CFStringCreateWithCString(kCFAllocatorDefault, deviceName, kCFStringEncodingASCII); 93 | 94 | // Dump our data to stdout just to see what it looks like. 95 | // 96 | CFShow(deviceNameAsCFString); 97 | 98 | privateDataRef->deviceName = deviceNameAsCFString; 99 | 100 | // Now, get the locationID of this device. In order to do this, we need to create an IOUSBDeviceInterface for 101 | // our device. This will create the necessary connections between our user land application and the kernel object 102 | // for the USB Device. 103 | // 104 | kr = IOCreatePlugInInterfaceForService(usbDevice, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score); 105 | 106 | if ((kIOReturnSuccess != kr) || !plugInInterface) 107 | { 108 | printf("unable to create a plugin (%08x)\n", kr); 109 | continue; 110 | } 111 | 112 | // I have the device plugin, I need the device interface 113 | // 114 | res = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), (LPVOID)&privateDataRef->deviceInterface); 115 | (*plugInInterface)->Release(plugInInterface); // done with this 116 | if (res || !privateDataRef->deviceInterface) 117 | { 118 | printf("couldn't create a device interface (%08x)\n", (int) res); 119 | continue; 120 | } 121 | 122 | // Now that we have the IOUSBDeviceInterface, we can call the routines in IOUSBLib.h 123 | // In this case, we just want the locationID. 124 | // 125 | kr = (*privateDataRef->deviceInterface)->GetLocationID(privateDataRef->deviceInterface, &locationID); 126 | if (KERN_SUCCESS != kr) 127 | { 128 | printf("GetLocationID returned %08x\n", kr); 129 | continue; 130 | } 131 | else 132 | { 133 | printf("Location ID: 0x%lx\n", (unsigned long)locationID); 134 | 135 | } 136 | 137 | privateDataRef->locationID = locationID; 138 | 139 | // Register for an interest notification for this device. Pass the reference to our 140 | // private data as the refCon for the notification. 141 | // 142 | kr = IOServiceAddInterestNotification( gNotifyPort, // notifyPort 143 | usbDevice, // service 144 | kIOGeneralInterest, // interestType 145 | DeviceNotification, // callback 146 | privateDataRef, // refCon 147 | &(privateDataRef->notification) // notification 148 | ); 149 | 150 | if (KERN_SUCCESS != kr) 151 | { 152 | printf("IOServiceAddInterestNotification returned 0x%08x\n", kr); 153 | } 154 | 155 | // Done with this io_service_t 156 | // 157 | kr = IOObjectRelease(usbDevice); 158 | 159 | free(privateDataRef); 160 | } 161 | } 162 | 163 | void SignalHandler(int sigraised) 164 | { 165 | printf("\nInterrupted\n"); 166 | 167 | // Clean up here 168 | IONotificationPortDestroy(gNotifyPort); 169 | 170 | if (gAddedIter) 171 | { 172 | IOObjectRelease(gAddedIter); 173 | gAddedIter = 0; 174 | } 175 | 176 | // exit(0) should not be called from a signal handler. Use _exit(0) instead 177 | // 178 | _exit(0); 179 | } 180 | 181 | //================================================================================================ 182 | // main 183 | //================================================================================================ 184 | // 185 | int main (int argc, const char *argv[]) 186 | { 187 | mach_port_t masterPort; 188 | CFMutableDictionaryRef matchingDict; 189 | CFRunLoopSourceRef runLoopSource; 190 | CFNumberRef numberRef; 191 | CFStringRef stringRef; 192 | kern_return_t kr; 193 | long usbVendor = kMyVendorID; 194 | sig_t oldHandler; 195 | 196 | // pick up command line arguments 197 | // 198 | if (argc > 1) 199 | usbVendor = atoi(argv[1]); 200 | 201 | // Set up a signal handler so we can clean up when we're interrupted from the command line 202 | // Otherwise we stay in our run loop forever. 203 | // 204 | oldHandler = signal(SIGINT, SignalHandler); 205 | if (oldHandler == SIG_ERR) 206 | printf("Could not establish new signal handler"); 207 | 208 | // first create a master_port for my task 209 | // 210 | kr = IOMasterPort(MACH_PORT_NULL, &masterPort); 211 | if (kr || !masterPort) 212 | { 213 | printf("ERR: Couldn't create a master IOKit Port(%08x)\n", kr); 214 | return -1; 215 | } 216 | 217 | printf("Looking for devices matching vendor ID=%ld\n", usbVendor); 218 | 219 | // Set up the matching criteria for the devices we're interested in. The matching criteria needs to follow 220 | // the same rules as kernel drivers: mainly it needs to follow the USB Common Class Specification, pp. 6-7. 221 | // See also http://developer.apple.com/qa/qa2001/qa1076.html 222 | // One exception is that you can use the matching dictionary "as is", i.e. without adding any matching criteria 223 | // to it and it will match every IOUSBDevice in the system. IOServiceAddMatchingNotification will consume this 224 | // dictionary reference, so there is no need to release it later on. 225 | // 226 | matchingDict = IOServiceMatching(kIOUSBDeviceClassName); // Interested in instances of class 227 | // IOUSBDevice and its subclasses 228 | if (!matchingDict) 229 | { 230 | printf("Can't create a USB matching dictionary\n"); 231 | mach_port_deallocate(mach_task_self(), masterPort); 232 | return -1; 233 | } 234 | 235 | // We are interested in all USB Devices (as opposed to USB interfaces). The Common Class Specification 236 | // tells us that we need to specify the idVendor, idProduct, and bcdDevice fields, or, if we're not interested 237 | // in particular bcdDevices, just the idVendor and idProduct. Note that if we were trying to match an IOUSBInterface, 238 | // we would need to set more values in the matching dictionary (e.g. idVendor, idProduct, bInterfaceNumber and 239 | // bConfigurationValue. 240 | // 241 | 242 | // Create a CFNumber for the idVendor and set the value in the dictionary 243 | // 244 | numberRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usbVendor); 245 | CFDictionarySetValue( 246 | matchingDict, 247 | CFSTR(kUSBVendorID), 248 | numberRef); 249 | CFRelease(numberRef); 250 | numberRef = 0; 251 | 252 | // Create a CFString for the wildcard product ID and set the value in the dictionary 253 | // 254 | stringRef = CFSTR("*"); 255 | CFDictionarySetValue( 256 | matchingDict, 257 | CFSTR(kUSBProductID), 258 | stringRef); 259 | CFRelease(stringRef); 260 | stringRef = 0; 261 | 262 | // Create a notification port and add its run loop event source to our run loop 263 | // This is how async notifications get set up. 264 | // 265 | gNotifyPort = IONotificationPortCreate(masterPort); 266 | runLoopSource = IONotificationPortGetRunLoopSource(gNotifyPort); 267 | 268 | gRunLoop = CFRunLoopGetCurrent(); 269 | CFRunLoopAddSource(gRunLoop, runLoopSource, kCFRunLoopDefaultMode); 270 | 271 | // Now set up a notification to be called when a device is first matched by I/O Kit. 272 | // Note that this will not catch any devices that were already plugged in so we take 273 | // care of those later. 274 | // 275 | kr = IOServiceAddMatchingNotification(gNotifyPort, // notifyPort 276 | kIOFirstMatchNotification, // notificationType 277 | matchingDict, // matching 278 | DeviceAdded, // callback 279 | NULL, // refCon 280 | &gAddedIter // notification 281 | ); 282 | 283 | // Iterate once to get already-present devices and arm the notification 284 | // 285 | DeviceAdded(NULL, gAddedIter); 286 | 287 | // Now done with the master_port 288 | mach_port_deallocate(mach_task_self(), masterPort); 289 | masterPort = 0; 290 | 291 | // Start the run loop. Now we'll receive notifications. 292 | // 293 | printf("Starting run loop.\n"); 294 | CFRunLoopRun(); 295 | 296 | // We should never get here 297 | // 298 | printf("Unexpectedly back from CFRunLoopRun()!\n"); 299 | 300 | return 0; 301 | } 302 | --------------------------------------------------------------------------------