python-fido2/fido_host/pyu2f/windows.py

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:]