├── .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 | 
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 |
--------------------------------------------------------------------------------