mirror of https://github.com/Yubico/python-fido2
370 lines
12 KiB
Python
370 lines
12 KiB
Python
# Copyright 2016 Google Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Implements raw HID device communication on Windows."""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
import platform
|
|
|
|
from . import base, errors
|
|
|
|
|
|
# Load relevant DLLs
|
|
hid = ctypes.windll.Hid
|
|
setupapi = ctypes.windll.SetupAPI
|
|
kernel32 = ctypes.windll.Kernel32
|
|
|
|
|
|
# Various structs that are used in the Windows APIs we call
|
|
class GUID(ctypes.Structure):
|
|
_fields_ = [('Data1', ctypes.c_ulong),
|
|
('Data2', ctypes.c_ushort),
|
|
('Data3', ctypes.c_ushort),
|
|
('Data4', ctypes.c_ubyte * 8)]
|
|
|
|
# On Windows, SetupAPI.h packs structures differently in 64bit and
|
|
# 32bit mode. In 64bit mode, thestructures are packed on 8 byte
|
|
# boundaries, while in 32bit mode, they are packed on 1 byte boundaries.
|
|
# This is important to get right for some API calls that fill out these
|
|
# structures.
|
|
if platform.architecture()[0] == '64bit':
|
|
SETUPAPI_PACK = 8
|
|
elif platform.architecture()[0] == '32bit':
|
|
SETUPAPI_PACK = 1
|
|
else:
|
|
raise errors.HidError('Unknown architecture: %s' % platform.architecture()[0])
|
|
|
|
class DeviceInterfaceData(ctypes.Structure):
|
|
_fields_ = [('cbSize', wintypes.DWORD),
|
|
('InterfaceClassGuid', GUID),
|
|
('Flags', wintypes.DWORD),
|
|
('Reserved', ctypes.POINTER(ctypes.c_ulong))]
|
|
_pack_ = SETUPAPI_PACK
|
|
|
|
|
|
class DeviceInterfaceDetailData(ctypes.Structure):
|
|
_fields_ = [('cbSize', wintypes.DWORD),
|
|
('DevicePath', ctypes.c_byte * 1)]
|
|
_pack_ = SETUPAPI_PACK
|
|
|
|
|
|
class HidAttributes(ctypes.Structure):
|
|
_fields_ = [('Size', ctypes.c_ulong),
|
|
('VendorID', ctypes.c_ushort),
|
|
('ProductID', ctypes.c_ushort),
|
|
('VersionNumber', ctypes.c_ushort)]
|
|
|
|
|
|
class HidCapabilities(ctypes.Structure):
|
|
_fields_ = [('Usage', ctypes.c_ushort),
|
|
('UsagePage', ctypes.c_ushort),
|
|
('InputReportByteLength', ctypes.c_ushort),
|
|
('OutputReportByteLength', ctypes.c_ushort),
|
|
('FeatureReportByteLength', ctypes.c_ushort),
|
|
('Reserved', ctypes.c_ushort * 17),
|
|
('NotUsed', ctypes.c_ushort * 10)]
|
|
|
|
# Various void* aliases for readability.
|
|
HDEVINFO = ctypes.c_void_p
|
|
HANDLE = ctypes.c_void_p
|
|
PHIDP_PREPARSED_DATA = ctypes.c_void_p # pylint: disable=invalid-name
|
|
|
|
# This is a HANDLE.
|
|
INVALID_HANDLE_VALUE = 0xffffffff
|
|
|
|
# Status codes
|
|
NTSTATUS = ctypes.c_long
|
|
HIDP_STATUS_SUCCESS = 0x00110000
|
|
FILE_SHARE_READ = 0x00000001
|
|
FILE_SHARE_WRITE = 0x00000002
|
|
OPEN_EXISTING = 0x03
|
|
ERROR_ACCESS_DENIED = 0x05
|
|
|
|
# CreateFile Flags
|
|
GENERIC_WRITE = 0x40000000
|
|
GENERIC_READ = 0x80000000
|
|
|
|
# Function signatures
|
|
hid.HidD_GetHidGuid.restype = None
|
|
hid.HidD_GetHidGuid.argtypes = [ctypes.POINTER(GUID)]
|
|
hid.HidD_GetAttributes.restype = wintypes.BOOLEAN
|
|
hid.HidD_GetAttributes.argtypes = [HANDLE, ctypes.POINTER(HidAttributes)]
|
|
hid.HidD_GetPreparsedData.restype = wintypes.BOOLEAN
|
|
hid.HidD_GetPreparsedData.argtypes = [HANDLE,
|
|
ctypes.POINTER(PHIDP_PREPARSED_DATA)]
|
|
hid.HidD_FreePreparsedData.restype = wintypes.BOOLEAN
|
|
hid.HidD_FreePreparsedData.argtypes = [PHIDP_PREPARSED_DATA]
|
|
hid.HidD_GetProductString.restype = wintypes.BOOLEAN
|
|
hid.HidD_GetProductString.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong]
|
|
hid.HidP_GetCaps.restype = NTSTATUS
|
|
hid.HidP_GetCaps.argtypes = [PHIDP_PREPARSED_DATA,
|
|
ctypes.POINTER(HidCapabilities)]
|
|
|
|
setupapi.SetupDiGetClassDevsA.argtypes = [ctypes.POINTER(GUID), ctypes.c_char_p,
|
|
wintypes.HWND, wintypes.DWORD]
|
|
setupapi.SetupDiGetClassDevsA.restype = HDEVINFO
|
|
setupapi.SetupDiEnumDeviceInterfaces.restype = wintypes.BOOL
|
|
setupapi.SetupDiEnumDeviceInterfaces.argtypes = [
|
|
HDEVINFO, ctypes.c_void_p, ctypes.POINTER(GUID), wintypes.DWORD,
|
|
ctypes.POINTER(DeviceInterfaceData)]
|
|
setupapi.SetupDiGetDeviceInterfaceDetailA.restype = wintypes.BOOL
|
|
setupapi.SetupDiGetDeviceInterfaceDetailA.argtypes = [
|
|
HDEVINFO, ctypes.POINTER(DeviceInterfaceData),
|
|
ctypes.POINTER(DeviceInterfaceDetailData), wintypes.DWORD,
|
|
ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p]
|
|
|
|
kernel32.CreateFileA.restype = HANDLE
|
|
kernel32.CreateFileA.argtypes = [
|
|
ctypes.c_char_p, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
|
|
wintypes.DWORD, wintypes.DWORD, HANDLE]
|
|
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
kernel32.CloseHandle.argtypes = [HANDLE]
|
|
kernel32.ReadFile.restype = wintypes.BOOL
|
|
kernel32.ReadFile.argtypes = [
|
|
HANDLE, ctypes.c_void_p, wintypes.DWORD,
|
|
ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p]
|
|
kernel32.WriteFile.restype = wintypes.BOOL
|
|
kernel32.WriteFile.argtypes = [
|
|
HANDLE, ctypes.c_void_p, wintypes.DWORD,
|
|
ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p]
|
|
|
|
|
|
def FillDeviceAttributes(device, descriptor):
|
|
"""Fill out the attributes of the device.
|
|
|
|
Fills the devices HidAttributes and product string
|
|
into the descriptor.
|
|
|
|
Args:
|
|
device: A handle to the open device
|
|
descriptor: The DeviceDescriptor to populate with the
|
|
attributes.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Raises:
|
|
WindowsError when unable to obtain attributes or product
|
|
string.
|
|
"""
|
|
attributes = HidAttributes()
|
|
result = hid.HidD_GetAttributes(device, ctypes.byref(attributes))
|
|
if not result:
|
|
raise ctypes.WinError()
|
|
|
|
buf = ctypes.create_string_buffer(1024)
|
|
result = hid.HidD_GetProductString(device, buf, 1024)
|
|
|
|
if not result:
|
|
raise ctypes.WinError()
|
|
|
|
descriptor.vendor_id = attributes.VendorID
|
|
descriptor.product_id = attributes.ProductID
|
|
descriptor.product_string = ctypes.wstring_at(buf)
|
|
|
|
|
|
def FillDeviceCapabilities(device, descriptor):
|
|
"""Fill out device capabilities.
|
|
|
|
Fills the HidCapabilitites of the device into descriptor.
|
|
|
|
Args:
|
|
device: A handle to the open device
|
|
descriptor: DeviceDescriptor to populate with the
|
|
capabilities
|
|
|
|
Returns:
|
|
none
|
|
|
|
Raises:
|
|
WindowsError when unable to obtain capabilitites.
|
|
"""
|
|
preparsed_data = PHIDP_PREPARSED_DATA(0)
|
|
ret = hid.HidD_GetPreparsedData(device, ctypes.byref(preparsed_data))
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
try:
|
|
caps = HidCapabilities()
|
|
ret = hid.HidP_GetCaps(preparsed_data, ctypes.byref(caps))
|
|
|
|
if ret != HIDP_STATUS_SUCCESS:
|
|
raise ctypes.WinError()
|
|
|
|
descriptor.usage = caps.Usage
|
|
descriptor.usage_page = caps.UsagePage
|
|
descriptor.internal_max_in_report_len = caps.InputReportByteLength
|
|
descriptor.internal_max_out_report_len = caps.OutputReportByteLength
|
|
|
|
finally:
|
|
hid.HidD_FreePreparsedData(preparsed_data)
|
|
|
|
|
|
# The python os.open() implementation uses the windows libc
|
|
# open() function, which writes CreateFile but does so in a way
|
|
# that doesn't let us open the device with the right set of permissions.
|
|
# Therefore, we have to directly use the Windows API calls.
|
|
# We could use PyWin32, which provides simple wrappers. However, to avoid
|
|
# requiring a PyWin32 dependency for clients, we simply also implement it
|
|
# using ctypes.
|
|
def OpenDevice(path, enum=False):
|
|
"""Open the device and return a handle to it."""
|
|
desired_access = GENERIC_WRITE | GENERIC_READ
|
|
share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE
|
|
if enum:
|
|
desired_access = 0
|
|
|
|
h = kernel32.CreateFileA(path,
|
|
desired_access,
|
|
share_mode,
|
|
None, OPEN_EXISTING, 0, None)
|
|
if h == INVALID_HANDLE_VALUE:
|
|
raise ctypes.WinError()
|
|
return h
|
|
|
|
|
|
class WindowsHidDevice(base.HidDevice):
|
|
"""Implementation of raw HID interface on Windows."""
|
|
|
|
@staticmethod
|
|
def Enumerate():
|
|
"""See base class."""
|
|
hid_guid = GUID()
|
|
hid.HidD_GetHidGuid(ctypes.byref(hid_guid))
|
|
|
|
devices = setupapi.SetupDiGetClassDevsA(
|
|
ctypes.byref(hid_guid), None, None, 0x12)
|
|
index = 0
|
|
interface_info = DeviceInterfaceData()
|
|
interface_info.cbSize = ctypes.sizeof(DeviceInterfaceData) # pylint: disable=invalid-name
|
|
|
|
out = []
|
|
while True:
|
|
result = setupapi.SetupDiEnumDeviceInterfaces(
|
|
devices, 0, ctypes.byref(hid_guid), index,
|
|
ctypes.byref(interface_info))
|
|
index += 1
|
|
if not result:
|
|
break
|
|
|
|
detail_len = wintypes.DWORD()
|
|
result = setupapi.SetupDiGetDeviceInterfaceDetailA(
|
|
devices, ctypes.byref(interface_info), None, 0,
|
|
ctypes.byref(detail_len), None)
|
|
|
|
detail_len = detail_len.value
|
|
if detail_len == 0:
|
|
# skip this device, some kind of error
|
|
continue
|
|
|
|
buf = ctypes.create_string_buffer(detail_len)
|
|
interface_detail = DeviceInterfaceDetailData.from_buffer(buf)
|
|
interface_detail.cbSize = ctypes.sizeof(DeviceInterfaceDetailData)
|
|
|
|
result = setupapi.SetupDiGetDeviceInterfaceDetailA(
|
|
devices, ctypes.byref(interface_info),
|
|
ctypes.byref(interface_detail), detail_len, None, None)
|
|
|
|
if not result:
|
|
raise ctypes.WinError()
|
|
|
|
descriptor = base.DeviceDescriptor()
|
|
# This is a bit of a hack to work around a limitation of ctypes and
|
|
# "header" structures that are common in windows. DevicePath is a
|
|
# ctypes array of length 1, but it is backed with a buffer that is much
|
|
# longer and contains a null terminated string. So, we read the null
|
|
# terminated string off DevicePath here. Per the comment above, the
|
|
# alignment of this struct varies depending on architecture, but
|
|
# in all cases the path string starts 1 DWORD into the structure.
|
|
#
|
|
# The path length is:
|
|
# length of detail buffer - header length (1 DWORD)
|
|
path_len = detail_len - ctypes.sizeof(wintypes.DWORD)
|
|
descriptor.path = ctypes.string_at(
|
|
ctypes.addressof(interface_detail.DevicePath), path_len)
|
|
|
|
device = None
|
|
try:
|
|
device = OpenDevice(descriptor.path, True)
|
|
except WindowsError as e: # pylint: disable=undefined-variable
|
|
if e.winerror == ERROR_ACCESS_DENIED: # Access Denied, e.g. a keyboard
|
|
continue
|
|
else:
|
|
raise e
|
|
|
|
try:
|
|
FillDeviceAttributes(device, descriptor)
|
|
FillDeviceCapabilities(device, descriptor)
|
|
out.append(descriptor.ToPublicDict())
|
|
except Exception:
|
|
continue # Try with next device
|
|
finally:
|
|
kernel32.CloseHandle(device)
|
|
|
|
return out
|
|
|
|
def __init__(self, path):
|
|
"""See base class."""
|
|
base.HidDevice.__init__(self, path)
|
|
self.dev = OpenDevice(path)
|
|
self.desc = base.DeviceDescriptor()
|
|
FillDeviceCapabilities(self.dev, self.desc)
|
|
|
|
def GetInReportDataLength(self):
|
|
"""See base class."""
|
|
return self.desc.internal_max_in_report_len - 1
|
|
|
|
def GetOutReportDataLength(self):
|
|
"""See base class."""
|
|
return self.desc.internal_max_out_report_len - 1
|
|
|
|
def Write(self, packet):
|
|
"""See base class."""
|
|
if len(packet) != self.GetOutReportDataLength():
|
|
raise errors.HidError('Packet length must match report data length.')
|
|
|
|
out = bytes(bytearray([0] + packet)) # Prepend the zero-byte (report ID)
|
|
num_written = wintypes.DWORD()
|
|
ret = (
|
|
kernel32.WriteFile(
|
|
self.dev, out, len(out),
|
|
ctypes.byref(num_written), None))
|
|
if num_written.value != len(out):
|
|
raise errors.HidError(
|
|
'Failed to write complete packet. ' + 'Expected %d, but got %d' %
|
|
(len(out), num_written.value))
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
def Read(self):
|
|
"""See base class."""
|
|
buf = ctypes.create_string_buffer(self.desc.internal_max_in_report_len)
|
|
num_read = wintypes.DWORD()
|
|
ret = kernel32.ReadFile(
|
|
self.dev, buf, len(buf), ctypes.byref(num_read), None)
|
|
|
|
if num_read.value != self.desc.internal_max_in_report_len:
|
|
raise errors.HidError('Failed to read full length report from device.')
|
|
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
# Convert the string buffer to a list of numbers. Throw away the first
|
|
# byte, which is the report id (which we don't care about).
|
|
return b''.join(buf)[1:]
|