1# Copyright 2016 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Implements HID device interface on MacOS using IOKit and HIDManager.""" 16from six.moves import queue 17from six.moves import range 18import ctypes 19import ctypes.util 20import logging 21import sys 22import threading 23 24from pyu2f import errors 25from pyu2f.hid import base 26 27logger = logging.getLogger('pyu2f.macos') 28 29# Constants 30DEVICE_PATH_BUFFER_SIZE = 512 31DEVICE_STRING_PROPERTY_BUFFER_SIZE = 512 32 33HID_DEVICE_PROPERTY_VENDOR_ID = 'VendorId' 34HID_DEVICE_PROPERTY_PRODUCT_ID = 'ProductID' 35HID_DEVICE_PROPERTY_PRODUCT = 'Product' 36HID_DEVICE_PROPERTY_PRIMARY_USAGE = 'PrimaryUsage' 37HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE = 'PrimaryUsagePage' 38HID_DEVICE_PROPERTY_MAX_INPUT_REPORT_SIZE = 'MaxInputReportSize' 39HID_DEVICE_PROPERTY_MAX_OUTPUT_REPORT_SIZE = 'MaxOutputReportSize' 40HID_DEVICE_PROPERTY_REPORT_ID = 'ReportID' 41 42 43# Declare C types 44class _CFType(ctypes.Structure): 45 pass 46 47 48class _CFString(_CFType): 49 pass 50 51 52class _CFSet(_CFType): 53 pass 54 55 56class _IOHIDManager(_CFType): 57 pass 58 59 60class _IOHIDDevice(_CFType): 61 pass 62 63 64class _CFRunLoop(_CFType): 65 pass 66 67 68class _CFAllocator(_CFType): 69 pass 70 71 72# Linter isn't consistent about valid class names. Disabling some of the errors 73CF_SET_REF = ctypes.POINTER(_CFSet) 74CF_STRING_REF = ctypes.POINTER(_CFString) 75CF_TYPE_REF = ctypes.POINTER(_CFType) 76CF_RUN_LOOP_REF = ctypes.POINTER(_CFRunLoop) 77CF_RUN_LOOP_RUN_RESULT = ctypes.c_int32 78CF_ALLOCATOR_REF = ctypes.POINTER(_CFAllocator) 79CF_TYPE_ID = ctypes.c_ulong # pylint: disable=invalid-name 80CF_INDEX = ctypes.c_long # pylint: disable=invalid-name 81CF_TIME_INTERVAL = ctypes.c_double # pylint: disable=invalid-name 82IO_RETURN = ctypes.c_uint 83IO_HID_REPORT_TYPE = ctypes.c_uint 84IO_OBJECT_T = ctypes.c_uint 85MACH_PORT_T = ctypes.c_uint 86IO_STRING_T = ctypes.c_char_p # pylint: disable=invalid-name 87IO_SERVICE_T = IO_OBJECT_T 88IO_REGISTRY_ENTRY_T = IO_OBJECT_T 89 90IO_HID_MANAGER_REF = ctypes.POINTER(_IOHIDManager) 91IO_HID_DEVICE_REF = ctypes.POINTER(_IOHIDDevice) 92 93IO_HID_REPORT_CALLBACK = ctypes.CFUNCTYPE(None, ctypes.py_object, IO_RETURN, 94 ctypes.c_void_p, IO_HID_REPORT_TYPE, 95 ctypes.c_uint32, 96 ctypes.POINTER(ctypes.c_uint8), 97 CF_INDEX) 98 99# Define C constants 100K_CF_NUMBER_SINT32_TYPE = 3 101K_CF_STRING_ENCODING_UTF8 = 0x08000100 102K_CF_ALLOCATOR_DEFAULT = None 103 104K_IO_SERVICE_PLANE = b'IOService' 105K_IO_MASTER_PORT_DEFAULT = 0 106K_IO_HID_REPORT_TYPE_OUTPUT = 1 107K_IO_RETURN_SUCCESS = 0 108 109K_CF_RUN_LOOP_RUN_STOPPED = 2 110K_CF_RUN_LOOP_RUN_TIMED_OUT = 3 111K_CF_RUN_LOOP_RUN_HANDLED_SOURCE = 4 112 113# Load relevant libraries 114iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) 115cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) 116 117# Only use iokit and cf if we're on macos, this way we can still run tests 118# on other platforms if we properly mock 119if sys.platform.startswith('darwin'): 120 # Exported constants 121 K_CF_RUNLOOP_DEFAULT_MODE = CF_STRING_REF.in_dll(cf, 'kCFRunLoopDefaultMode') 122 123 # Declare C function prototypes 124 cf.CFSetGetValues.restype = None 125 cf.CFSetGetValues.argtypes = [CF_SET_REF, ctypes.POINTER(ctypes.c_void_p)] 126 cf.CFStringCreateWithCString.restype = CF_STRING_REF 127 cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, 128 ctypes.c_uint32] 129 cf.CFStringGetCString.restype = ctypes.c_int 130 cf.CFStringGetCString.argtypes = [CF_STRING_REF, ctypes.c_char_p, CF_INDEX, 131 ctypes.c_uint32] 132 cf.CFStringGetLength.restype = CF_INDEX 133 cf.CFStringGetLength.argtypes = [CF_STRING_REF] 134 cf.CFGetTypeID.restype = CF_TYPE_ID 135 cf.CFGetTypeID.argtypes = [CF_TYPE_REF] 136 cf.CFNumberGetTypeID.restype = CF_TYPE_ID 137 cf.CFNumberGetValue.restype = ctypes.c_int 138 cf.CFRelease.restype = None 139 cf.CFRelease.argtypes = [CF_TYPE_REF] 140 cf.CFRunLoopGetCurrent.restype = CF_RUN_LOOP_REF 141 cf.CFRunLoopGetCurrent.argTypes = [] 142 cf.CFRunLoopRunInMode.restype = CF_RUN_LOOP_RUN_RESULT 143 cf.CFRunLoopRunInMode.argtypes = [CF_STRING_REF, CF_TIME_INTERVAL, 144 ctypes.c_bool] 145 146 iokit.IOObjectRelease.argtypes = [IO_OBJECT_T] 147 iokit.IOHIDManagerCreate.restype = IO_HID_MANAGER_REF 148 iokit.IOHIDManagerCopyDevices.restype = CF_SET_REF 149 iokit.IOHIDManagerCopyDevices.argtypes = [IO_HID_MANAGER_REF] 150 iokit.IOHIDManagerSetDeviceMatching.restype = None 151 iokit.IOHIDManagerSetDeviceMatching.argtypes = [IO_HID_MANAGER_REF, 152 CF_TYPE_REF] 153 iokit.IOHIDDeviceGetProperty.restype = CF_TYPE_REF 154 iokit.IOHIDDeviceGetProperty.argtypes = [IO_HID_DEVICE_REF, CF_STRING_REF] 155 iokit.IOHIDDeviceRegisterInputReportCallback.restype = None 156 iokit.IOHIDDeviceRegisterInputReportCallback.argtypes = [ 157 IO_HID_DEVICE_REF, ctypes.POINTER(ctypes.c_uint8), CF_INDEX, 158 IO_HID_REPORT_CALLBACK, ctypes.py_object] 159 iokit.IORegistryEntryFromPath.restype = IO_REGISTRY_ENTRY_T 160 iokit.IORegistryEntryFromPath.argtypes = [MACH_PORT_T, IO_STRING_T] 161 iokit.IOHIDDeviceCreate.restype = IO_HID_DEVICE_REF 162 iokit.IOHIDDeviceCreate.argtypes = [CF_ALLOCATOR_REF, IO_SERVICE_T] 163 iokit.IOHIDDeviceScheduleWithRunLoop.restype = None 164 iokit.IOHIDDeviceScheduleWithRunLoop.argtypes = [IO_HID_DEVICE_REF, 165 CF_RUN_LOOP_REF, 166 CF_STRING_REF] 167 iokit.IOHIDDeviceUnscheduleFromRunLoop.restype = None 168 iokit.IOHIDDeviceUnscheduleFromRunLoop.argtypes = [IO_HID_DEVICE_REF, 169 CF_RUN_LOOP_REF, 170 CF_STRING_REF] 171 iokit.IOHIDDeviceSetReport.restype = IO_RETURN 172 iokit.IOHIDDeviceSetReport.argtypes = [IO_HID_DEVICE_REF, IO_HID_REPORT_TYPE, 173 CF_INDEX, 174 ctypes.POINTER(ctypes.c_uint8), 175 CF_INDEX] 176else: 177 logger.warn('Not running on MacOS') 178 179 180def CFStr(s): 181 """Builds a CFString from a python string. 182 183 Args: 184 s: source string 185 186 Returns: 187 CFStringRef representation of the source string 188 189 Resulting CFString must be CFReleased when no longer needed. 190 """ 191 return cf.CFStringCreateWithCString(None, s.encode(), 0) 192 193 194def GetDeviceIntProperty(dev_ref, key): 195 """Reads int property from the HID device.""" 196 cf_key = CFStr(key) 197 type_ref = iokit.IOHIDDeviceGetProperty(dev_ref, cf_key) 198 cf.CFRelease(cf_key) 199 if not type_ref: 200 return None 201 202 if cf.CFGetTypeID(type_ref) != cf.CFNumberGetTypeID(): 203 raise errors.OsHidError('Expected number type, got {}'.format( 204 cf.CFGetTypeID(type_ref))) 205 206 out = ctypes.c_int32() 207 ret = cf.CFNumberGetValue(type_ref, K_CF_NUMBER_SINT32_TYPE, 208 ctypes.byref(out)) 209 if not ret: 210 return None 211 212 return out.value 213 214 215def GetDeviceStringProperty(dev_ref, key): 216 """Reads string property from the HID device.""" 217 cf_key = CFStr(key) 218 type_ref = iokit.IOHIDDeviceGetProperty(dev_ref, cf_key) 219 cf.CFRelease(cf_key) 220 if not type_ref: 221 return None 222 223 if cf.CFGetTypeID(type_ref) != cf.CFStringGetTypeID(): 224 raise errors.OsHidError('Expected string type, got {}'.format( 225 cf.CFGetTypeID(type_ref))) 226 227 type_ref = ctypes.cast(type_ref, CF_STRING_REF) 228 out = ctypes.create_string_buffer(DEVICE_STRING_PROPERTY_BUFFER_SIZE) 229 ret = cf.CFStringGetCString(type_ref, out, DEVICE_STRING_PROPERTY_BUFFER_SIZE, 230 K_CF_STRING_ENCODING_UTF8) 231 if not ret: 232 return None 233 234 return out.value.decode('utf8') 235 236 237def GetDevicePath(device_handle): 238 """Obtains the unique path for the device. 239 240 Args: 241 device_handle: reference to the device 242 243 Returns: 244 A unique path for the device, obtained from the IO Registry 245 246 """ 247 # Obtain device path from IO Registry 248 io_service_obj = iokit.IOHIDDeviceGetService(device_handle) 249 str_buffer = ctypes.create_string_buffer(DEVICE_PATH_BUFFER_SIZE) 250 iokit.IORegistryEntryGetPath(io_service_obj, K_IO_SERVICE_PLANE, str_buffer) 251 252 return str_buffer.value 253 254 255def HidReadCallback(read_queue, result, sender, report_type, report_id, report, 256 report_length): 257 """Handles incoming IN report from HID device.""" 258 del result, sender, report_type, report_id # Unused by the callback function 259 260 incoming_bytes = [report[i] for i in range(report_length)] 261 read_queue.put(incoming_bytes) 262 263# C wrapper around ReadCallback() 264# Declared in this scope so it doesn't get GC-ed 265REGISTERED_READ_CALLBACK = IO_HID_REPORT_CALLBACK(HidReadCallback) 266 267 268def DeviceReadThread(hid_device): 269 """Binds a device to the thread's run loop, then starts the run loop. 270 271 Args: 272 hid_device: The MacOsHidDevice object 273 274 The HID manager requires a run loop to handle Report reads. This thread 275 function serves that purpose. 276 """ 277 278 # Schedule device events with run loop 279 hid_device.run_loop_ref = cf.CFRunLoopGetCurrent() 280 if not hid_device.run_loop_ref: 281 logger.error('Failed to get current run loop') 282 return 283 284 iokit.IOHIDDeviceScheduleWithRunLoop(hid_device.device_handle, 285 hid_device.run_loop_ref, 286 K_CF_RUNLOOP_DEFAULT_MODE) 287 288 # Run the run loop 289 run_loop_run_result = K_CF_RUN_LOOP_RUN_TIMED_OUT 290 while (run_loop_run_result == K_CF_RUN_LOOP_RUN_TIMED_OUT or 291 run_loop_run_result == K_CF_RUN_LOOP_RUN_HANDLED_SOURCE): 292 run_loop_run_result = cf.CFRunLoopRunInMode( 293 K_CF_RUNLOOP_DEFAULT_MODE, 294 1000, # Timeout in seconds 295 False) # Return after source handled 296 297 # log any unexpected run loop exit 298 if run_loop_run_result != K_CF_RUN_LOOP_RUN_STOPPED: 299 logger.error('Unexpected run loop exit code: %d', run_loop_run_result) 300 301 # Unschedule from run loop 302 iokit.IOHIDDeviceUnscheduleFromRunLoop(hid_device.device_handle, 303 hid_device.run_loop_ref, 304 K_CF_RUNLOOP_DEFAULT_MODE) 305 306 307class MacOsHidDevice(base.HidDevice): 308 """Implementation of HID device for MacOS. 309 310 Uses IOKit HID Manager to interact with the device. 311 """ 312 313 @staticmethod 314 def Enumerate(): 315 """See base class.""" 316 # Init a HID manager 317 hid_mgr = iokit.IOHIDManagerCreate(None, None) 318 if not hid_mgr: 319 raise errors.OsHidError('Unable to obtain HID manager reference') 320 iokit.IOHIDManagerSetDeviceMatching(hid_mgr, None) 321 322 # Get devices from HID manager 323 device_set_ref = iokit.IOHIDManagerCopyDevices(hid_mgr) 324 if not device_set_ref: 325 raise errors.OsHidError('Failed to obtain devices from HID manager') 326 327 num = iokit.CFSetGetCount(device_set_ref) 328 devices = (IO_HID_DEVICE_REF * num)() 329 iokit.CFSetGetValues(device_set_ref, devices) 330 331 # Retrieve and build descriptor dictionaries for each device 332 descriptors = [] 333 for dev in devices: 334 d = base.DeviceDescriptor() 335 d.vendor_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_VENDOR_ID) 336 d.product_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRODUCT_ID) 337 d.product_string = GetDeviceStringProperty(dev, 338 HID_DEVICE_PROPERTY_PRODUCT) 339 d.usage = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE) 340 d.usage_page = GetDeviceIntProperty( 341 dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE) 342 d.report_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_REPORT_ID) 343 d.path = GetDevicePath(dev) 344 descriptors.append(d.ToPublicDict()) 345 346 # Clean up CF objects 347 cf.CFRelease(device_set_ref) 348 cf.CFRelease(hid_mgr) 349 350 return descriptors 351 352 def __init__(self, path): 353 # Resolve the path to device handle 354 device_entry = iokit.IORegistryEntryFromPath(K_IO_MASTER_PORT_DEFAULT, path) 355 if not device_entry: 356 raise errors.OsHidError('Device path does not match any HID device on ' 357 'the system') 358 359 self.device_handle = iokit.IOHIDDeviceCreate(K_CF_ALLOCATOR_DEFAULT, 360 device_entry) 361 if not self.device_handle: 362 raise errors.OsHidError('Failed to obtain device handle from registry ' 363 'entry') 364 iokit.IOObjectRelease(device_entry) 365 366 self.device_path = path 367 368 # Open device 369 result = iokit.IOHIDDeviceOpen(self.device_handle, 0) 370 if result != K_IO_RETURN_SUCCESS: 371 raise errors.OsHidError('Failed to open device for communication: {}' 372 .format(result)) 373 374 # Create read queue 375 self.read_queue = queue.Queue() 376 377 # Create and start read thread 378 self.run_loop_ref = None 379 self.read_thread = threading.Thread(target=DeviceReadThread, 380 args=(self,)) 381 self.read_thread.daemon = True 382 self.read_thread.start() 383 384 # Read max report sizes for in/out 385 self.internal_max_in_report_len = GetDeviceIntProperty( 386 self.device_handle, 387 HID_DEVICE_PROPERTY_MAX_INPUT_REPORT_SIZE) 388 if not self.internal_max_in_report_len: 389 raise errors.OsHidError('Unable to obtain max in report size') 390 391 self.internal_max_out_report_len = GetDeviceIntProperty( 392 self.device_handle, 393 HID_DEVICE_PROPERTY_MAX_OUTPUT_REPORT_SIZE) 394 if not self.internal_max_out_report_len: 395 raise errors.OsHidError('Unable to obtain max out report size') 396 397 # Register read callback 398 self.in_report_buffer = (ctypes.c_uint8 * self.internal_max_in_report_len)() 399 iokit.IOHIDDeviceRegisterInputReportCallback( 400 self.device_handle, 401 self.in_report_buffer, 402 self.internal_max_in_report_len, 403 REGISTERED_READ_CALLBACK, 404 ctypes.py_object(self.read_queue)) 405 406 def GetInReportDataLength(self): 407 """See base class.""" 408 return self.internal_max_in_report_len 409 410 def GetOutReportDataLength(self): 411 """See base class.""" 412 return self.internal_max_out_report_len 413 414 def Write(self, packet): 415 """See base class.""" 416 report_id = 0 417 out_report_buffer = (ctypes.c_uint8 * self.internal_max_out_report_len)() 418 out_report_buffer[:] = packet[:] 419 420 result = iokit.IOHIDDeviceSetReport(self.device_handle, 421 K_IO_HID_REPORT_TYPE_OUTPUT, 422 report_id, 423 out_report_buffer, 424 self.internal_max_out_report_len) 425 426 # Non-zero status indicates failure 427 if result != K_IO_RETURN_SUCCESS: 428 raise errors.OsHidError('Failed to write report to device') 429 430 def Read(self): 431 """See base class.""" 432 433 result = None 434 while result is None: 435 try: 436 result = self.read_queue.get(timeout=60) 437 except queue.Empty: 438 continue 439 440 return result 441 442 def __del__(self): 443 # Unregister the callback 444 if hasattr(self, 'in_report_buffer'): 445 iokit.IOHIDDeviceRegisterInputReportCallback( 446 self.device_handle, 447 self.in_report_buffer, 448 self.internal_max_in_report_len, 449 None, 450 None) 451 452 # Stop the run loop 453 if hasattr(self, 'run_loop_ref'): 454 cf.CFRunLoopStop(self.run_loop_ref) 455 456 # Wait for the read thread to exit 457 if hasattr(self, 'read_thread'): 458 self.read_thread.join() 459