pyu2f-0.1.5/0000775000372000037200000000000013747070572013440 5ustar travistravis00000000000000pyu2f-0.1.5/MANIFEST.in0000664000372000037200000000002013747070544015165 0ustar travistravis00000000000000include LICENSE pyu2f-0.1.5/pyu2f.egg-info/0000775000372000037200000000000013747070572016177 5ustar travistravis00000000000000pyu2f-0.1.5/pyu2f.egg-info/PKG-INFO0000664000372000037200000000157113747070572017300 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: pyu2f Version: 0.1.5 Summary: U2F host library for interacting with a U2F device over USB. Home-page: https://github.com/google/pyu2f/ Author: Google Inc. Author-email: pyu2f-team@google.com License: Apache 2.0 Description: pyu2f is a python based U2F host library for Linux, Windows, and MacOS. It provides functionality for interacting with a U2F device over USB. Platform: Windows Platform: Linux Platform: OS X Platform: macOS Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 pyu2f-0.1.5/pyu2f.egg-info/zip-safe0000664000372000037200000000000113747070572017627 0ustar travistravis00000000000000 pyu2f-0.1.5/pyu2f.egg-info/dependency_links.txt0000664000372000037200000000000113747070572022245 0ustar travistravis00000000000000 pyu2f-0.1.5/pyu2f.egg-info/top_level.txt0000664000372000037200000000000613747070572020725 0ustar travistravis00000000000000pyu2f pyu2f-0.1.5/pyu2f.egg-info/SOURCES.txt0000664000372000037200000000117113747070572020063 0ustar travistravis00000000000000LICENSE MANIFEST.in README.md setup.cfg setup.py pyu2f/__init__.py pyu2f/apdu.py pyu2f/errors.py pyu2f/hardware.py pyu2f/hidtransport.py pyu2f/model.py pyu2f/u2f.py pyu2f.egg-info/PKG-INFO pyu2f.egg-info/SOURCES.txt pyu2f.egg-info/dependency_links.txt pyu2f.egg-info/requires.txt pyu2f.egg-info/top_level.txt pyu2f.egg-info/zip-safe pyu2f/convenience/__init__.py pyu2f/convenience/authenticator.py pyu2f/convenience/baseauthenticator.py pyu2f/convenience/customauthenticator.py pyu2f/convenience/localauthenticator.py pyu2f/hid/__init__.py pyu2f/hid/base.py pyu2f/hid/linux.py pyu2f/hid/macos.py pyu2f/hid/try.py pyu2f/hid/windows.pypyu2f-0.1.5/pyu2f.egg-info/requires.txt0000664000372000037200000000000413747070572020571 0ustar travistravis00000000000000six pyu2f-0.1.5/PKG-INFO0000664000372000037200000000157113747070572014541 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: pyu2f Version: 0.1.5 Summary: U2F host library for interacting with a U2F device over USB. Home-page: https://github.com/google/pyu2f/ Author: Google Inc. Author-email: pyu2f-team@google.com License: Apache 2.0 Description: pyu2f is a python based U2F host library for Linux, Windows, and MacOS. It provides functionality for interacting with a U2F device over USB. Platform: Windows Platform: Linux Platform: OS X Platform: macOS Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 pyu2f-0.1.5/pyu2f/0000775000372000037200000000000013747070572014505 5ustar travistravis00000000000000pyu2f-0.1.5/pyu2f/u2f.py0000664000372000037200000001557513747070544015567 0ustar travistravis00000000000000# 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. """Implement a high level U2F API analogous to the javascript API spec. This modules implements a high level U2F API that is analogous in spirit to the high level U2F javascript API. It supports both registration and authetication. For the purposes of this API, the "origin" is the hostname of the machine this library is running on. """ import hashlib import socket import time from pyu2f import errors from pyu2f import hardware from pyu2f import hidtransport from pyu2f import model def GetLocalU2FInterface(origin=socket.gethostname()): """Obtains a U2FInterface for the first valid local U2FHID device found.""" hid_transports = hidtransport.DiscoverLocalHIDU2FDevices() for t in hid_transports: try: return U2FInterface(security_key=hardware.SecurityKey(transport=t), origin=origin) except errors.UnsupportedVersionException: # Skip over devices that don't speak the proper version of the protocol. pass # Unable to find a device raise errors.NoDeviceFoundError() class U2FInterface(object): """High level U2F interface. Implements a high level interface in the spirit of the FIDO U2F javascript API high level interface. It supports registration and authentication (signing). IMPORTANT NOTE: This class does NOT validate the app id against the origin. In particular, any user can assert any app id all the way to the device. The security model of a python library is such that doing so would not provide significant benfit as it could be bypassed by the caller talking to a lower level of the API. In fact, so could the origin itself. The origin is still set to a plausible value (the hostname) by this library. TODO(gdasher): Figure out a plan on how to address this gap/document the consequences of this more clearly. """ def __init__(self, security_key, origin=socket.gethostname()): self.origin = origin self.security_key = security_key if self.security_key.CmdVersion() != b'U2F_V2': raise errors.UnsupportedVersionException() def Register(self, app_id, challenge, registered_keys): """Registers app_id with the security key. Executes the U2F registration flow with the security key. Args: app_id: The app_id to register the security key against. challenge: Server challenge passed to the security key. registered_keys: List of keys already registered for this app_id+user. Returns: RegisterResponse with key_handle and attestation information in it ( encoded in FIDO U2F binary format within registration_data field). Raises: U2FError: There was some kind of problem with registration (e.g. the device was already registered or there was a timeout waiting for the test of user presence). """ client_data = model.ClientData(model.ClientData.TYP_REGISTRATION, challenge, self.origin) challenge_param = self.InternalSHA256(client_data.GetJson()) app_param = self.InternalSHA256(app_id) for key in registered_keys: try: # skip non U2F_V2 keys if key.version != u'U2F_V2': continue resp = self.security_key.CmdAuthenticate(challenge_param, app_param, key.key_handle, True) # check_only mode CmdAuthenticate should always raise some # exception raise errors.HardwareError('Should Never Happen') except errors.TUPRequiredError: # This indicates key was valid. Thus, no need to register raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) except errors.InvalidKeyHandleError as e: # This is the case of a key for a different token, so we just ignore it. pass except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) # Now register the new key for _ in range(30): try: resp = self.security_key.CmdRegister(challenge_param, app_param) return model.RegisterResponse(resp, client_data) except errors.TUPRequiredError as e: self.security_key.CmdWink() time.sleep(0.5) except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) raise errors.U2FError(errors.U2FError.TIMEOUT) def Authenticate(self, app_id, challenge, registered_keys): """Authenticates app_id with the security key. Executes the U2F authentication/signature flow with the security key. Args: app_id: The app_id to register the security key against. challenge: Server challenge passed to the security key as a bytes object. registered_keys: List of keys already registered for this app_id+user. Returns: SignResponse with client_data, key_handle, and signature_data. The client data is an object, while the signature_data is encoded in FIDO U2F binary format. Raises: U2FError: There was some kind of problem with authentication (e.g. there was a timeout while waiting for the test of user presence.) """ client_data = model.ClientData(model.ClientData.TYP_AUTHENTICATION, challenge, self.origin) app_param = self.InternalSHA256(app_id) challenge_param = self.InternalSHA256(client_data.GetJson()) num_invalid_keys = 0 for key in registered_keys: try: if key.version != u'U2F_V2': continue for _ in range(30): try: resp = self.security_key.CmdAuthenticate(challenge_param, app_param, key.key_handle) return model.SignResponse(key.key_handle, resp, client_data) except errors.TUPRequiredError: self.security_key.CmdWink() time.sleep(0.5) except errors.InvalidKeyHandleError: num_invalid_keys += 1 continue except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) if num_invalid_keys == len(registered_keys): # In this case, all provided keys were invalid. raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) # In this case, the TUP was not pressed. raise errors.U2FError(errors.U2FError.TIMEOUT) def InternalSHA256(self, string): md = hashlib.sha256() md.update(string.encode()) return md.digest() pyu2f-0.1.5/pyu2f/convenience/0000775000372000037200000000000013747070572017001 5ustar travistravis00000000000000pyu2f-0.1.5/pyu2f/convenience/localauthenticator.py0000664000372000037200000000471313747070544023244 0ustar travistravis00000000000000# 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. """Convenience class for U2F signing with local security keys.""" import six import base64 import sys from pyu2f import errors from pyu2f import u2f from pyu2f.convenience import baseauthenticator class LocalAuthenticator(baseauthenticator.BaseAuthenticator): """Authenticator wrapper around the native python u2f implementation.""" def __init__(self, origin): self.origin = origin def Authenticate(self, app_id, challenge_data, print_callback=sys.stderr.write): """See base class.""" # If authenticator is not plugged in, prompt try: device = u2f.GetLocalU2FInterface(origin=self.origin) except errors.NoDeviceFoundError: print_callback('Please insert your security key and press enter...') six.moves.input() device = u2f.GetLocalU2FInterface(origin=self.origin) print_callback('Please touch your security key.\n') for challenge_item in challenge_data: raw_challenge = challenge_item['challenge'] key = challenge_item['key'] try: result = device.Authenticate(app_id, raw_challenge, [key]) except errors.U2FError as e: if e.code == errors.U2FError.DEVICE_INELIGIBLE: continue else: raise client_data = self._base64encode(result.client_data.GetJson().encode()) signature_data = self._base64encode(result.signature_data) key_handle = self._base64encode(result.key_handle) return { 'clientData': client_data, 'signatureData': signature_data, 'applicationId': app_id, 'keyHandle': key_handle, } raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) def IsAvailable(self): """See base class.""" return True def _base64encode(self, bytes_data): """Helper method to base64 encode and return str result.""" return base64.urlsafe_b64encode(bytes_data).decode() pyu2f-0.1.5/pyu2f/convenience/customauthenticator.py0000664000372000037200000002033713747070544023464 0ustar travistravis00000000000000# 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. """Class to offload the end to end flow of U2F signing.""" import base64 import hashlib import json import os import struct import subprocess import sys from pyu2f import errors from pyu2f import model from pyu2f.convenience import baseauthenticator SK_SIGNING_PLUGIN_ENV_VAR = 'SK_SIGNING_PLUGIN' U2F_SIGNATURE_TIMEOUT_SECONDS = 5 SK_SIGNING_PLUGIN_NO_ERROR = 0 SK_SIGNING_PLUGIN_TOUCH_REQUIRED = 0x6985 SK_SIGNING_PLUGIN_WRONG_DATA = 0x6A80 class CustomAuthenticator(baseauthenticator.BaseAuthenticator): """Offloads U2F signing to a pluggable command-line tool. Offloads U2F signing to a signing plugin which takes the form of a command-line tool. The command-line tool is configurable via the SK_SIGNING_PLUGIN environment variable. The signing plugin should implement the following interface: Communication occurs over stdin/stdout, and messages are both sent and received in the form: [4 bytes - payload size (little-endian)][variable bytes - json payload] Signing Request JSON { "type": "sign_helper_request", "signData": [{ "keyHandle": , "appIdHash": , "challengeHash": , "version": U2F protocol version (usually "U2F_V2") },...], "timeoutSeconds": } Signing Response JSON { "type": "sign_helper_reply", "code": . "errorDetail": , "responseData": { "appIdHash": , "challengeHash": , "keyHandle": , "version": , "signatureData": } } Possible response error codes are: NoError = 0 UnknownError = -127 TouchRequired = 0x6985 WrongData = 0x6a80 """ def __init__(self, origin): self.origin = origin def Authenticate(self, app_id, challenge_data, print_callback=sys.stderr.write): """See base class.""" # Ensure environment variable is present plugin_cmd = os.environ.get(SK_SIGNING_PLUGIN_ENV_VAR) if plugin_cmd is None: raise errors.PluginError('{} env var is not set' .format(SK_SIGNING_PLUGIN_ENV_VAR)) # Prepare input to signer client_data_map, signing_input = self._BuildPluginRequest( app_id, challenge_data, self.origin) # Call plugin print_callback('Please insert and touch your security key\n') response = self._CallPlugin([plugin_cmd], signing_input) # Handle response key_challenge_pair = (response['keyHandle'], response['challengeHash']) client_data_json = client_data_map[key_challenge_pair] client_data = client_data_json.encode() return self._BuildAuthenticatorResponse(app_id, client_data, response) def IsAvailable(self): """See base class.""" return os.environ.get(SK_SIGNING_PLUGIN_ENV_VAR) is not None def _BuildPluginRequest(self, app_id, challenge_data, origin): """Builds a JSON request in the form that the plugin expects.""" client_data_map = {} encoded_challenges = [] app_id_hash_encoded = self._Base64Encode(self._SHA256(app_id)) for challenge_item in challenge_data: key = challenge_item['key'] key_handle_encoded = self._Base64Encode(key.key_handle) raw_challenge = challenge_item['challenge'] client_data_json = model.ClientData( model.ClientData.TYP_AUTHENTICATION, raw_challenge, origin).GetJson() challenge_hash_encoded = self._Base64Encode( self._SHA256(client_data_json)) # Populate challenges list encoded_challenges.append({ 'appIdHash': app_id_hash_encoded, 'challengeHash': challenge_hash_encoded, 'keyHandle': key_handle_encoded, 'version': key.version, }) # Populate ClientData map key_challenge_pair = (key_handle_encoded, challenge_hash_encoded) client_data_map[key_challenge_pair] = client_data_json signing_request = { 'type': 'sign_helper_request', 'signData': encoded_challenges, 'timeoutSeconds': U2F_SIGNATURE_TIMEOUT_SECONDS, 'localAlways': True } return client_data_map, json.dumps(signing_request) def _BuildAuthenticatorResponse(self, app_id, client_data, plugin_response): """Builds the response to return to the caller.""" encoded_client_data = self._Base64Encode(client_data) signature_data = str(plugin_response['signatureData']) key_handle = str(plugin_response['keyHandle']) response = { 'clientData': encoded_client_data, 'signatureData': signature_data, 'applicationId': app_id, 'keyHandle': key_handle, } return response def _CallPlugin(self, cmd, input_json): """Calls the plugin and validates the response.""" # Calculate length of input input_length = len(input_json) length_bytes_le = struct.pack('= pos + 1 + value_length: report_count = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_SIZE: if len(rd) >= pos + 1 + value_length: report_size = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == USAGE_PAGE: if len(rd) >= pos + 1 + value_length: usage_page = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == USAGE: if len(rd) >= pos + 1 + value_length: usage = ReadLsbBytes(rd, pos + 1, value_length) pos += value_length + key_size return desc def ParseUevent(uevent, desc): lines = uevent.split(b'\n') for line in lines: line = line.strip() if not line: continue k, v = line.split(b'=') if k == b'HID_NAME': desc.product_string = v.decode('utf8') elif k == b'HID_ID': _, vid, pid = v.split(b':') desc.vendor_id = int(vid, 16) desc.product_id = int(pid, 16) class LinuxHidDevice(base.HidDevice): """Implementation of HID device for linux. Implementation of HID device interface for linux that uses block devices to interact with the device and sysfs to enumerate/discover device metadata. """ @staticmethod def Enumerate(): for hidraw in os.listdir('/sys/class/hidraw'): rd_path = ( os.path.join( '/sys/class/hidraw', hidraw, 'device/report_descriptor')) uevent_path = os.path.join('/sys/class/hidraw', hidraw, 'device/uevent') rd_file = open(rd_path, 'rb') uevent_file = open(uevent_path, 'rb') desc = base.DeviceDescriptor() desc.path = os.path.join('/dev/', hidraw) ParseReportDescriptor(rd_file.read(), desc) ParseUevent(uevent_file.read(), desc) rd_file.close() uevent_file.close() yield desc.ToPublicDict() def __init__(self, path): base.HidDevice.__init__(self, path) self.dev = os.open(path, os.O_RDWR) self.desc = base.DeviceDescriptor() self.desc.path = path rd_file = open(os.path.join('/sys/class/hidraw', os.path.basename(path), 'device/report_descriptor'), 'rb') ParseReportDescriptor(rd_file.read(), self.desc) rd_file.close() def GetInReportDataLength(self): """See base class.""" return self.desc.internal_max_in_report_len def GetOutReportDataLength(self): """See base class.""" return self.desc.internal_max_out_report_len def Write(self, packet): """See base class.""" out = bytearray([0] + packet) # Prepend the zero-byte (report ID) os.write(self.dev, out) def Read(self): """See base class.""" raw_in = os.read(self.dev, self.GetInReportDataLength()) decoded_in = list(bytearray(raw_in)) return decoded_in pyu2f-0.1.5/pyu2f/hid/windows.py0000664000372000037200000003060413747070544017317 0ustar travistravis00000000000000# 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.""" import ctypes from ctypes import wintypes import platform from pyu2f import errors from pyu2f.hid import base # 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()) 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.") packet_data = [0] + packet # Prepend the zero-byte (report ID) out = bytes(bytearray(packet_data)) 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 list(bytearray(buf[1:])) def __del__(self): """Closes the file handle when object is GC-ed.""" if hasattr(self, 'dev'): kernel32.CloseHandle(self.dev) pyu2f-0.1.5/pyu2f/hid/base.py0000664000372000037200000000523413747070544016540 0ustar travistravis00000000000000# 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. """Implement base classes for hid package. This module provides the base classes implemented by the platform-specific modules. It includes a base class for all implementations built on interacting with file-like objects. """ class HidDevice(object): """Base class for all HID devices in this package.""" @staticmethod def Enumerate(): """Enumerates all the hid devices. This function enumerates all the hid device and provides metadata for helping the client select one. Returns: A list of dictionaries of metadata. Each implementation is required to provide at least: vendor_id, product_id, product_string, usage, usage_page, and path. """ pass def __init__(self, path): """Initialize the device at path.""" pass def GetInReportDataLength(self): """Returns the max input report data length in bytes. Returns the max input report data length in bytes. This excludes the report id. """ pass def GetOutReportDataLength(self): """Returns the max output report data length in bytes. Returns the max output report data length in bytes. This excludes the report id. """ pass def Write(self, packet): """Writes packet to device. Writes the packet to the device. Args: packet: An array of integers to write to the device. Excludes the report ID. Must be equal to GetOutReportLength(). """ pass def Read(self): """Reads packet from device. Reads the packet from the device. Returns: An array of integers read from the device. Excludes the report ID. The length is equal to GetInReportDataLength(). """ pass class DeviceDescriptor(object): """Descriptor for basic attributes of the device.""" usage_page = None usage = None vendor_id = None product_id = None product_string = None path = None internal_max_in_report_len = 0 internal_max_out_report_len = 0 def ToPublicDict(self): out = {} for k, v in list(self.__dict__.items()): if not k.startswith('internal_'): out[k] = v return out pyu2f-0.1.5/pyu2f/hid/__init__.py0000664000372000037200000000307613747070544017367 0ustar travistravis00000000000000# 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 interface for talking to hid devices. This module implenets an interface for talking to low level hid devices using various methods on different platforms. """ import sys def Enumerate(): return InternalPlatformSwitch('Enumerate') def Open(path): return InternalPlatformSwitch('__init__', path) def InternalPlatformSwitch(funcname, *args, **kwargs): """Determine, on a platform-specific basis, which module to use.""" # pylint: disable=g-import-not-at-top clz = None if sys.platform.startswith('linux'): from pyu2f.hid import linux clz = linux.LinuxHidDevice elif sys.platform.startswith('win32'): from pyu2f.hid import windows clz = windows.WindowsHidDevice elif sys.platform.startswith('darwin'): from pyu2f.hid import macos clz = macos.MacOsHidDevice if not clz: raise Exception('Unsupported platform: ' + sys.platform) if funcname == '__init__': return clz(*args, **kwargs) return getattr(clz, funcname)(*args, **kwargs) pyu2f-0.1.5/pyu2f/hid/macos.py0000664000372000037200000003714413747070544016735 0ustar travistravis00000000000000# 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 HID device interface on MacOS using IOKit and HIDManager.""" from six.moves import queue from six.moves import range import ctypes import ctypes.util import logging import sys import threading from pyu2f import errors from pyu2f.hid import base logger = logging.getLogger('pyu2f.macos') # Constants DEVICE_PATH_BUFFER_SIZE = 512 DEVICE_STRING_PROPERTY_BUFFER_SIZE = 512 HID_DEVICE_PROPERTY_VENDOR_ID = 'VendorId' HID_DEVICE_PROPERTY_PRODUCT_ID = 'ProductID' HID_DEVICE_PROPERTY_PRODUCT = 'Product' HID_DEVICE_PROPERTY_PRIMARY_USAGE = 'PrimaryUsage' HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE = 'PrimaryUsagePage' HID_DEVICE_PROPERTY_MAX_INPUT_REPORT_SIZE = 'MaxInputReportSize' HID_DEVICE_PROPERTY_MAX_OUTPUT_REPORT_SIZE = 'MaxOutputReportSize' HID_DEVICE_PROPERTY_REPORT_ID = 'ReportID' # Declare C types class _CFType(ctypes.Structure): pass class _CFString(_CFType): pass class _CFSet(_CFType): pass class _IOHIDManager(_CFType): pass class _IOHIDDevice(_CFType): pass class _CFRunLoop(_CFType): pass class _CFAllocator(_CFType): pass # Linter isn't consistent about valid class names. Disabling some of the errors CF_SET_REF = ctypes.POINTER(_CFSet) CF_STRING_REF = ctypes.POINTER(_CFString) CF_TYPE_REF = ctypes.POINTER(_CFType) CF_RUN_LOOP_REF = ctypes.POINTER(_CFRunLoop) CF_RUN_LOOP_RUN_RESULT = ctypes.c_int32 CF_ALLOCATOR_REF = ctypes.POINTER(_CFAllocator) CF_TYPE_ID = ctypes.c_ulong # pylint: disable=invalid-name CF_INDEX = ctypes.c_long # pylint: disable=invalid-name CF_TIME_INTERVAL = ctypes.c_double # pylint: disable=invalid-name IO_RETURN = ctypes.c_uint IO_HID_REPORT_TYPE = ctypes.c_uint IO_OBJECT_T = ctypes.c_uint MACH_PORT_T = ctypes.c_uint IO_STRING_T = ctypes.c_char_p # pylint: disable=invalid-name IO_SERVICE_T = IO_OBJECT_T IO_REGISTRY_ENTRY_T = IO_OBJECT_T IO_HID_MANAGER_REF = ctypes.POINTER(_IOHIDManager) IO_HID_DEVICE_REF = ctypes.POINTER(_IOHIDDevice) IO_HID_REPORT_CALLBACK = ctypes.CFUNCTYPE(None, ctypes.py_object, IO_RETURN, ctypes.c_void_p, IO_HID_REPORT_TYPE, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint8), CF_INDEX) # Define C constants K_CF_NUMBER_SINT32_TYPE = 3 K_CF_STRING_ENCODING_UTF8 = 0x08000100 K_CF_ALLOCATOR_DEFAULT = None K_IO_SERVICE_PLANE = b'IOService' K_IO_MASTER_PORT_DEFAULT = 0 K_IO_HID_REPORT_TYPE_OUTPUT = 1 K_IO_RETURN_SUCCESS = 0 K_CF_RUN_LOOP_RUN_STOPPED = 2 K_CF_RUN_LOOP_RUN_TIMED_OUT = 3 K_CF_RUN_LOOP_RUN_HANDLED_SOURCE = 4 # Load relevant libraries iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) # Only use iokit and cf if we're on macos, this way we can still run tests # on other platforms if we properly mock if sys.platform.startswith('darwin'): # Exported constants K_CF_RUNLOOP_DEFAULT_MODE = CF_STRING_REF.in_dll(cf, 'kCFRunLoopDefaultMode') # Declare C function prototypes cf.CFSetGetValues.restype = None cf.CFSetGetValues.argtypes = [CF_SET_REF, ctypes.POINTER(ctypes.c_void_p)] cf.CFStringCreateWithCString.restype = CF_STRING_REF cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32] cf.CFStringGetCString.restype = ctypes.c_int cf.CFStringGetCString.argtypes = [CF_STRING_REF, ctypes.c_char_p, CF_INDEX, ctypes.c_uint32] cf.CFStringGetLength.restype = CF_INDEX cf.CFStringGetLength.argtypes = [CF_STRING_REF] cf.CFGetTypeID.restype = CF_TYPE_ID cf.CFGetTypeID.argtypes = [CF_TYPE_REF] cf.CFNumberGetTypeID.restype = CF_TYPE_ID cf.CFNumberGetValue.restype = ctypes.c_int cf.CFRelease.restype = None cf.CFRelease.argtypes = [CF_TYPE_REF] cf.CFRunLoopGetCurrent.restype = CF_RUN_LOOP_REF cf.CFRunLoopGetCurrent.argTypes = [] cf.CFRunLoopRunInMode.restype = CF_RUN_LOOP_RUN_RESULT cf.CFRunLoopRunInMode.argtypes = [CF_STRING_REF, CF_TIME_INTERVAL, ctypes.c_bool] iokit.IOObjectRelease.argtypes = [IO_OBJECT_T] iokit.IOHIDManagerCreate.restype = IO_HID_MANAGER_REF iokit.IOHIDManagerCopyDevices.restype = CF_SET_REF iokit.IOHIDManagerCopyDevices.argtypes = [IO_HID_MANAGER_REF] iokit.IOHIDManagerSetDeviceMatching.restype = None iokit.IOHIDManagerSetDeviceMatching.argtypes = [IO_HID_MANAGER_REF, CF_TYPE_REF] iokit.IOHIDDeviceGetProperty.restype = CF_TYPE_REF iokit.IOHIDDeviceGetProperty.argtypes = [IO_HID_DEVICE_REF, CF_STRING_REF] iokit.IOHIDDeviceRegisterInputReportCallback.restype = None iokit.IOHIDDeviceRegisterInputReportCallback.argtypes = [ IO_HID_DEVICE_REF, ctypes.POINTER(ctypes.c_uint8), CF_INDEX, IO_HID_REPORT_CALLBACK, ctypes.py_object] iokit.IORegistryEntryFromPath.restype = IO_REGISTRY_ENTRY_T iokit.IORegistryEntryFromPath.argtypes = [MACH_PORT_T, IO_STRING_T] iokit.IOHIDDeviceCreate.restype = IO_HID_DEVICE_REF iokit.IOHIDDeviceCreate.argtypes = [CF_ALLOCATOR_REF, IO_SERVICE_T] iokit.IOHIDDeviceScheduleWithRunLoop.restype = None iokit.IOHIDDeviceScheduleWithRunLoop.argtypes = [IO_HID_DEVICE_REF, CF_RUN_LOOP_REF, CF_STRING_REF] iokit.IOHIDDeviceUnscheduleFromRunLoop.restype = None iokit.IOHIDDeviceUnscheduleFromRunLoop.argtypes = [IO_HID_DEVICE_REF, CF_RUN_LOOP_REF, CF_STRING_REF] iokit.IOHIDDeviceSetReport.restype = IO_RETURN iokit.IOHIDDeviceSetReport.argtypes = [IO_HID_DEVICE_REF, IO_HID_REPORT_TYPE, CF_INDEX, ctypes.POINTER(ctypes.c_uint8), CF_INDEX] else: logger.warn('Not running on MacOS') def CFStr(s): """Builds a CFString from a python string. Args: s: source string Returns: CFStringRef representation of the source string Resulting CFString must be CFReleased when no longer needed. """ return cf.CFStringCreateWithCString(None, s.encode(), 0) def GetDeviceIntProperty(dev_ref, key): """Reads int property from the HID device.""" cf_key = CFStr(key) type_ref = iokit.IOHIDDeviceGetProperty(dev_ref, cf_key) cf.CFRelease(cf_key) if not type_ref: return None if cf.CFGetTypeID(type_ref) != cf.CFNumberGetTypeID(): raise errors.OsHidError('Expected number type, got {}'.format( cf.CFGetTypeID(type_ref))) out = ctypes.c_int32() ret = cf.CFNumberGetValue(type_ref, K_CF_NUMBER_SINT32_TYPE, ctypes.byref(out)) if not ret: return None return out.value def GetDeviceStringProperty(dev_ref, key): """Reads string property from the HID device.""" cf_key = CFStr(key) type_ref = iokit.IOHIDDeviceGetProperty(dev_ref, cf_key) cf.CFRelease(cf_key) if not type_ref: return None if cf.CFGetTypeID(type_ref) != cf.CFStringGetTypeID(): raise errors.OsHidError('Expected string type, got {}'.format( cf.CFGetTypeID(type_ref))) type_ref = ctypes.cast(type_ref, CF_STRING_REF) out = ctypes.create_string_buffer(DEVICE_STRING_PROPERTY_BUFFER_SIZE) ret = cf.CFStringGetCString(type_ref, out, DEVICE_STRING_PROPERTY_BUFFER_SIZE, K_CF_STRING_ENCODING_UTF8) if not ret: return None return out.value.decode('utf8') def GetDevicePath(device_handle): """Obtains the unique path for the device. Args: device_handle: reference to the device Returns: A unique path for the device, obtained from the IO Registry """ # Obtain device path from IO Registry io_service_obj = iokit.IOHIDDeviceGetService(device_handle) str_buffer = ctypes.create_string_buffer(DEVICE_PATH_BUFFER_SIZE) iokit.IORegistryEntryGetPath(io_service_obj, K_IO_SERVICE_PLANE, str_buffer) return str_buffer.value def HidReadCallback(read_queue, result, sender, report_type, report_id, report, report_length): """Handles incoming IN report from HID device.""" del result, sender, report_type, report_id # Unused by the callback function incoming_bytes = [report[i] for i in range(report_length)] read_queue.put(incoming_bytes) # C wrapper around ReadCallback() # Declared in this scope so it doesn't get GC-ed REGISTERED_READ_CALLBACK = IO_HID_REPORT_CALLBACK(HidReadCallback) def DeviceReadThread(hid_device): """Binds a device to the thread's run loop, then starts the run loop. Args: hid_device: The MacOsHidDevice object The HID manager requires a run loop to handle Report reads. This thread function serves that purpose. """ # Schedule device events with run loop hid_device.run_loop_ref = cf.CFRunLoopGetCurrent() if not hid_device.run_loop_ref: logger.error('Failed to get current run loop') return iokit.IOHIDDeviceScheduleWithRunLoop(hid_device.device_handle, hid_device.run_loop_ref, K_CF_RUNLOOP_DEFAULT_MODE) # Run the run loop run_loop_run_result = K_CF_RUN_LOOP_RUN_TIMED_OUT while (run_loop_run_result == K_CF_RUN_LOOP_RUN_TIMED_OUT or run_loop_run_result == K_CF_RUN_LOOP_RUN_HANDLED_SOURCE): run_loop_run_result = cf.CFRunLoopRunInMode( K_CF_RUNLOOP_DEFAULT_MODE, 1000, # Timeout in seconds False) # Return after source handled # log any unexpected run loop exit if run_loop_run_result != K_CF_RUN_LOOP_RUN_STOPPED: logger.error('Unexpected run loop exit code: %d', run_loop_run_result) # Unschedule from run loop iokit.IOHIDDeviceUnscheduleFromRunLoop(hid_device.device_handle, hid_device.run_loop_ref, K_CF_RUNLOOP_DEFAULT_MODE) class MacOsHidDevice(base.HidDevice): """Implementation of HID device for MacOS. Uses IOKit HID Manager to interact with the device. """ @staticmethod def Enumerate(): """See base class.""" # Init a HID manager hid_mgr = iokit.IOHIDManagerCreate(None, None) if not hid_mgr: raise errors.OsHidError('Unable to obtain HID manager reference') iokit.IOHIDManagerSetDeviceMatching(hid_mgr, None) # Get devices from HID manager device_set_ref = iokit.IOHIDManagerCopyDevices(hid_mgr) if not device_set_ref: raise errors.OsHidError('Failed to obtain devices from HID manager') num = iokit.CFSetGetCount(device_set_ref) devices = (IO_HID_DEVICE_REF * num)() iokit.CFSetGetValues(device_set_ref, devices) # Retrieve and build descriptor dictionaries for each device descriptors = [] for dev in devices: d = base.DeviceDescriptor() d.vendor_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_VENDOR_ID) d.product_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRODUCT_ID) d.product_string = GetDeviceStringProperty(dev, HID_DEVICE_PROPERTY_PRODUCT) d.usage = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE) d.usage_page = GetDeviceIntProperty( dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE) d.report_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_REPORT_ID) d.path = GetDevicePath(dev) descriptors.append(d.ToPublicDict()) # Clean up CF objects cf.CFRelease(device_set_ref) cf.CFRelease(hid_mgr) return descriptors def __init__(self, path): # Resolve the path to device handle device_entry = iokit.IORegistryEntryFromPath(K_IO_MASTER_PORT_DEFAULT, path) if not device_entry: raise errors.OsHidError('Device path does not match any HID device on ' 'the system') self.device_handle = iokit.IOHIDDeviceCreate(K_CF_ALLOCATOR_DEFAULT, device_entry) if not self.device_handle: raise errors.OsHidError('Failed to obtain device handle from registry ' 'entry') iokit.IOObjectRelease(device_entry) self.device_path = path # Open device result = iokit.IOHIDDeviceOpen(self.device_handle, 0) if result != K_IO_RETURN_SUCCESS: raise errors.OsHidError('Failed to open device for communication: {}' .format(result)) # Create read queue self.read_queue = queue.Queue() # Create and start read thread self.run_loop_ref = None self.read_thread = threading.Thread(target=DeviceReadThread, args=(self,)) self.read_thread.daemon = True self.read_thread.start() # Read max report sizes for in/out self.internal_max_in_report_len = GetDeviceIntProperty( self.device_handle, HID_DEVICE_PROPERTY_MAX_INPUT_REPORT_SIZE) if not self.internal_max_in_report_len: raise errors.OsHidError('Unable to obtain max in report size') self.internal_max_out_report_len = GetDeviceIntProperty( self.device_handle, HID_DEVICE_PROPERTY_MAX_OUTPUT_REPORT_SIZE) if not self.internal_max_out_report_len: raise errors.OsHidError('Unable to obtain max out report size') # Register read callback self.in_report_buffer = (ctypes.c_uint8 * self.internal_max_in_report_len)() iokit.IOHIDDeviceRegisterInputReportCallback( self.device_handle, self.in_report_buffer, self.internal_max_in_report_len, REGISTERED_READ_CALLBACK, ctypes.py_object(self.read_queue)) def GetInReportDataLength(self): """See base class.""" return self.internal_max_in_report_len def GetOutReportDataLength(self): """See base class.""" return self.internal_max_out_report_len def Write(self, packet): """See base class.""" report_id = 0 out_report_buffer = (ctypes.c_uint8 * self.internal_max_out_report_len)() out_report_buffer[:] = packet[:] result = iokit.IOHIDDeviceSetReport(self.device_handle, K_IO_HID_REPORT_TYPE_OUTPUT, report_id, out_report_buffer, self.internal_max_out_report_len) # Non-zero status indicates failure if result != K_IO_RETURN_SUCCESS: raise errors.OsHidError('Failed to write report to device') def Read(self): """See base class.""" result = None while result is None: try: result = self.read_queue.get(timeout=60) except queue.Empty: continue return result def __del__(self): # Unregister the callback if hasattr(self, 'in_report_buffer'): iokit.IOHIDDeviceRegisterInputReportCallback( self.device_handle, self.in_report_buffer, self.internal_max_in_report_len, None, None) # Stop the run loop if hasattr(self, 'run_loop_ref'): cf.CFRunLoopStop(self.run_loop_ref) # Wait for the read thread to exit if hasattr(self, 'read_thread'): self.read_thread.join() pyu2f-0.1.5/pyu2f/hid/try.py0000664000372000037200000000157213747070544016445 0ustar travistravis00000000000000# 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. """Simple test program for hid interface. This simple test program lists all of the HID devices on the local system. """ from __future__ import print_function from pyu2f import hid def main(): devs = hid.Enumerate() for dev in devs: print(dev) if __name__ == '__main__': main() pyu2f-0.1.5/pyu2f/apdu.py0000664000372000037200000000755113747070544016017 0ustar travistravis00000000000000# 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. """Implement the U2F variant of ISO 7816 extended APDU. This module implements a subset ISO 7816 APDU encoding. In particular, it only supports extended length encoding, it only supports commands that expect a reply, and it does not support explicitly specifying the length of the expected reply. It also implements the U2F variant of ISO 7816 where the Lc field is always specified, even if there is no data. """ import struct from pyu2f import errors CMD_REGISTER = 0x01 CMD_AUTH = 0x02 CMD_VERSION = 0x03 class CommandApdu(object): """Represents a Command APDU. Represents a Command APDU sent to the security key. Encoding is specified in FIDO U2F standards. """ cla = None ins = None p1 = None p2 = None data = None def __init__(self, cla, ins, p1, p2, data=None): self.cla = cla self.ins = ins self.p1 = p1 self.p2 = p2 if data and len(data) > 65535: raise errors.InvalidCommandError() if data: self.data = data def ToByteArray(self): """Serialize the command. Encodes the command as per the U2F specs, using the standard ISO 7816-4 extended encoding. All Commands expect data, so Le is always present. Returns: Python bytearray of the encoded command. """ lc = self.InternalEncodeLc() out = bytearray(4) # will extend out[0] = self.cla out[1] = self.ins out[2] = self.p1 out[3] = self.p2 if self.data: out.extend(lc) out.extend(self.data) out.extend([0x00, 0x00]) # Le else: out.extend([0x00, 0x00, 0x00]) # Le return out def ToLegacyU2FByteArray(self): """Serialize the command in the legacy format. Encodes the command as per the U2F specs, using the legacy encoding in which LC is always present. Returns: Python bytearray of the encoded command. """ lc = self.InternalEncodeLc() out = bytearray(4) # will extend out[0] = self.cla out[1] = self.ins out[2] = self.p1 out[3] = self.p2 out.extend(lc) if self.data: out.extend(self.data) out.extend([0x00, 0x00]) # Le return out def InternalEncodeLc(self): dl = 0 if self.data: dl = len(self.data) # The top two bytes are guaranteed to be 0 by the assertion # in the constructor. fourbyte = struct.pack('>I', dl) return bytearray(fourbyte[1:]) class ResponseApdu(object): """Represents a Response APDU. Represents a Response APU sent by the security key. Encoding is specified in FIDO U2F standards. """ body = None sw1 = None sw2 = None def __init__(self, data): self.dbg_full_packet = data if not data or len(data) < 2: raise errors.InvalidResponseError() if len(data) > 2: self.body = data[:-2] self.sw1 = data[-2] self.sw2 = data[-1] def IsSuccess(self): return self.sw1 == 0x90 and self.sw2 == 0x00 def CheckSuccessOrRaise(self): if self.sw1 == 0x69 and self.sw2 == 0x85: # SW_CONDITIONS_NOT_SATISFIED raise errors.TUPRequiredError() elif self.sw1 == 0x6a and self.sw2 == 0x80: # SW_WRONG_DATA raise errors.InvalidKeyHandleError() elif self.sw1 == 0x67 and self.sw2 == 0x00: # SW_WRONG_LENGTH raise errors.InvalidKeyHandleError() elif not self.IsSuccess(): raise errors.ApduError(self.sw1, self.sw2) pyu2f-0.1.5/pyu2f/__init__.py0000664000372000037200000000121313747070544016612 0ustar travistravis00000000000000# 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. """Library for Universal 2nd factor authentication.""" pyu2f-0.1.5/pyu2f/hardware.py0000664000372000037200000001310313747070544016651 0ustar travistravis00000000000000# 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. """This module implements the low level device API. This module exposes a low level SecurityKey class, representing the physical security key device. """ import logging from pyu2f import apdu from pyu2f import errors class SecurityKey(object): """Low level api for talking to a security key. This class implements the low level api specified in FIDO U2F for talking to a security key. """ def __init__(self, transport): self.transport = transport self.use_legacy_format = False self.logger = logging.getLogger('pyu2f.hardware') def CmdRegister(self, challenge_param, app_param): """Register security key. Ask the security key to register with a particular origin & client. Args: challenge_param: Arbitrary 32 byte challenge string. app_param: Arbitrary 32 byte applciation parameter. Returns: A binary structure containing the key handle, attestation, and a signature over that by the attestation key. The precise format is dictated by the FIDO U2F specs. Raises: TUPRequiredError: A Test of User Precense is required to proceed. ApduError: Something went wrong on the device. """ self.logger.debug('CmdRegister') if len(challenge_param) != 32 or len(app_param) != 32: raise errors.InvalidRequestError() body = bytearray(challenge_param + app_param) response = self.InternalSendApdu(apdu.CommandApdu( 0, apdu.CMD_REGISTER, 0x03, # Per the U2F reference code tests 0x00, body)) response.CheckSuccessOrRaise() return response.body def CmdAuthenticate(self, challenge_param, app_param, key_handle, check_only=False): """Attempt to obtain an authentication signature. Ask the security key to sign a challenge for a particular key handle in order to authenticate the user. Args: challenge_param: SHA-256 hash of client_data object as a bytes object. app_param: SHA-256 hash of the app id as a bytes object. key_handle: The key handle to use to issue the signature as a bytes object. check_only: If true, only check if key_handle is valid. Returns: A binary structure containing the key handle, attestation, and a signature over that by the attestation key. The precise format is dictated by the FIDO U2F specs. Raises: TUPRequiredError: If check_only is False, a Test of User Precense is required to proceed. If check_only is True, this means the key_handle is valid. InvalidKeyHandleError: The key_handle is not valid for this device. ApduError: Something else went wrong on the device. """ self.logger.debug('CmdAuthenticate') if len(challenge_param) != 32 or len(app_param) != 32: raise errors.InvalidRequestError() control = 0x07 if check_only else 0x03 body = bytearray(challenge_param + app_param + bytearray([len(key_handle)]) + key_handle) response = self.InternalSendApdu(apdu.CommandApdu( 0, apdu.CMD_AUTH, control, 0x00, body)) response.CheckSuccessOrRaise() return response.body def CmdVersion(self): """Obtain the version of the device and test transport format. Obtains the version of the device and determines whether to use ISO 7816-4 or the U2f variant. This function should be called at least once before CmdAuthenticate or CmdRegister to make sure the object is using the proper transport for the device. Returns: The version of the U2F protocol in use. """ self.logger.debug('CmdVersion') response = self.InternalSendApdu(apdu.CommandApdu( 0, apdu.CMD_VERSION, 0x00, 0x00)) if not response.IsSuccess(): raise errors.ApduError(response.sw1, response.sw2) return response.body def CmdBlink(self, time): self.logger.debug('CmdBlink') self.transport.SendBlink(time) def CmdWink(self): self.logger.debug('CmdWink') self.transport.SendWink() def CmdPing(self, data): self.logger.debug('CmdPing') return self.transport.SendPing(data) def InternalSendApdu(self, apdu_to_send): """Send an APDU to the device. Sends an APDU to the device, possibly falling back to the legacy encoding format that is not ISO7816-4 compatible. Args: apdu_to_send: The CommandApdu object to send Returns: The ResponseApdu object constructed out of the devices reply. """ response = None if not self.use_legacy_format: response = apdu.ResponseApdu(self.transport.SendMsgBytes( apdu_to_send.ToByteArray())) if response.sw1 == 0x67 and response.sw2 == 0x00: # If we failed using the standard format, retry with the # legacy format. self.use_legacy_format = True return self.InternalSendApdu(apdu_to_send) else: response = apdu.ResponseApdu(self.transport.SendMsgBytes( apdu_to_send.ToLegacyU2FByteArray())) return response pyu2f-0.1.5/pyu2f/errors.py0000664000372000037200000000415213747070544016374 0ustar travistravis00000000000000# 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. """Exceptions that can be raised by the pyu2f library. All exceptions that can be raised by the pyu2f library. Most of these are internal coditions, but U2FError and NoDeviceFoundError are public errors that clients should expect to handle. """ class NoDeviceFoundError(Exception): pass class U2FError(Exception): OK = 0 OTHER_ERROR = 1 BAD_REQUEST = 2 CONFIGURATION_UNSUPPORTED = 3 DEVICE_INELIGIBLE = 4 TIMEOUT = 5 def __init__(self, code, cause=None): self.code = code if cause: self.cause = cause super(U2FError, self).__init__("U2F Error code: %d (cause: %s)" % (code, str(cause))) class HidError(Exception): """Errors in the hid usb transport protocol.""" pass class InvalidPacketError(HidError): pass class HardwareError(Exception): """Errors in the security key hardware that are transport independent.""" pass class InvalidRequestError(HardwareError): pass class ApduError(HardwareError): def __init__(self, sw1, sw2): self.sw1 = sw1 self.sw2 = sw2 super(ApduError, self).__init__("Device returned status: %d %d" % (sw1, sw2)) class TUPRequiredError(HardwareError): pass class InvalidKeyHandleError(HardwareError): pass class UnsupportedVersionException(Exception): pass class InvalidCommandError(Exception): pass class InvalidResponseError(Exception): pass class InvalidModelError(Exception): pass class OsHidError(Exception): pass class PluginError(Exception): pass pyu2f-0.1.5/pyu2f/model.py0000664000372000037200000000512413747070544016160 0ustar travistravis00000000000000# 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 data model for the library. This module implements basic data model objects that are necessary for interacting with the Security Key as well as for implementing the higher level components of the U2F protocol. """ import base64 import json from pyu2f import errors class ClientData(object): """FIDO U2F ClientData. Implements the ClientData object of the FIDO U2F protocol. """ TYP_AUTHENTICATION = 'navigator.id.getAssertion' TYP_REGISTRATION = 'navigator.id.finishEnrollment' def __init__(self, typ, raw_server_challenge, origin): if typ not in [ClientData.TYP_REGISTRATION, ClientData.TYP_AUTHENTICATION]: raise errors.InvalidModelError() self.typ = typ self.raw_server_challenge = raw_server_challenge self.origin = origin def GetJson(self): """Returns JSON version of ClientData compatible with FIDO spec.""" # The U2F Raw Messages specification specifies that the challenge is encoded # with URL safe Base64 without padding encoding specified in RFC 4648. # Python does not natively support a paddingless encoding, so we simply # remove the padding from the end of the string. server_challenge_b64 = base64.urlsafe_b64encode( self.raw_server_challenge).decode() server_challenge_b64 = server_challenge_b64.rstrip('=') return json.dumps({'typ': self.typ, 'challenge': server_challenge_b64, 'origin': self.origin}, sort_keys=True) def __repr__(self): return self.GetJson() class RegisteredKey(object): def __init__(self, key_handle, version=u'U2F_V2'): self.key_handle = key_handle self.version = version class RegisterResponse(object): def __init__(self, registration_data, client_data): self.registration_data = registration_data self.client_data = client_data class SignResponse(object): def __init__(self, key_handle, signature_data, client_data): self.key_handle = key_handle self.signature_data = signature_data self.client_data = client_data pyu2f-0.1.5/pyu2f/hidtransport.py0000664000372000037200000002630213747070544017602 0ustar travistravis00000000000000# 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. """HID Transport for U2F. This module imports the U2F HID Transport protocol as well as methods for discovering devices implementing this protocol. """ import logging import os import struct import time from pyu2f import errors from pyu2f import hid def HidUsageSelector(device): if device['usage_page'] == 0xf1d0 and device['usage'] == 0x01: return True return False def DiscoverLocalHIDU2FDevices(selector=HidUsageSelector): for d in hid.Enumerate(): if selector(d): try: dev = hid.Open(d['path']) yield UsbHidTransport(dev) except OSError: # Insufficient permissions to access device pass class UsbHidTransport(object): """Implements the U2FHID transport protocol. This class implements the U2FHID transport protocol from the FIDO U2F specs. This protocol manages fragmenting longer messages over a short hid frame (usually 64 bytes). It exposes an APDU channel through the MSG command as well as a series of other commands for configuring and interacting with the device. """ U2FHID_PING = 0x81 U2FHID_MSG = 0x83 U2FHID_WINK = 0x88 U2FHID_PROMPT = 0x87 U2FHID_INIT = 0x86 U2FHID_LOCK = 0x84 U2FHID_ERROR = 0xbf U2FHID_SYNC = 0xbc U2FHID_BROADCAST_CID = bytearray([0xff, 0xff, 0xff, 0xff]) ERR_CHANNEL_BUSY = bytearray([0x06]) class InitPacket(object): """Represent an initial U2FHID packet. Represent an initial U2FHID packet. This packet contains metadata necessary to interpret the entire packet stream associated with a particular exchange (read or write). Attributes: packet_size: The size of the hid report (packet) used. Usually 64. cid: The channel id for the connection to the device. size: The size of the entire message to be sent (including all continuation packets) payload: The portion of the message to put into the init packet. This must be smaller than packet_size - 7 (the overhead for an init packet). """ def __init__(self, packet_size, cid, cmd, size, payload): self.packet_size = packet_size if len(cid) != 4 or cmd > 255 or size >= 2**16: raise errors.InvalidPacketError() if len(payload) > self.packet_size - 7: raise errors.InvalidPacketError() self.cid = cid # byte array self.cmd = cmd # number self.size = size # number (full size of message) self.payload = payload # byte array (for first packet) def ToWireFormat(self): """Serializes the packet.""" ret = bytearray(64) ret[0:4] = self.cid ret[4] = self.cmd struct.pack_into('>H', ret, 5, self.size) ret[7:7 + len(self.payload)] = self.payload return list(map(int, ret)) @staticmethod def FromWireFormat(packet_size, data): """Derializes the packet. Deserializes the packet from wire format. Args: packet_size: The size of all packets (usually 64) data: List of ints or bytearray containing the data from the wire. Returns: InitPacket object for specified data Raises: InvalidPacketError: if the data isn't a valid InitPacket """ ba = bytearray(data) if len(ba) != packet_size: raise errors.InvalidPacketError() cid = ba[0:4] cmd = ba[4] size = struct.unpack('>H', bytes(ba[5:7]))[0] payload = ba[7:7 + size] # might truncate at packet_size return UsbHidTransport.InitPacket(packet_size, cid, cmd, size, payload) class ContPacket(object): """Represents a continutation U2FHID packet. Represents a continutation U2FHID packet. These packets follow the intial packet and contains the remaining data in a particular message. Attributes: packet_size: The size of the hid report (packet) used. Usually 64. cid: The channel id for the connection to the device. seq: The sequence number for this continuation packet. The first continuation packet is 0 and it increases from there. payload: The payload to put into this continuation packet. This must be less than packet_size - 5 (the overhead of the continuation packet is 5). """ def __init__(self, packet_size, cid, seq, payload): self.packet_size = packet_size self.cid = cid self.seq = seq self.payload = payload if len(payload) > self.packet_size - 5: raise errors.InvalidPacketError() if seq > 127: raise errors.InvalidPacketError() def ToWireFormat(self): """Serializes the packet.""" ret = bytearray(self.packet_size) ret[0:4] = self.cid ret[4] = self.seq ret[5:5 + len(self.payload)] = self.payload return list(map(int, ret)) @staticmethod def FromWireFormat(packet_size, data): """Derializes the packet. Deserializes the packet from wire format. Args: packet_size: The size of all packets (usually 64) data: List of ints or bytearray containing the data from the wire. Returns: InitPacket object for specified data Raises: InvalidPacketError: if the data isn't a valid ContPacket """ ba = bytearray(data) if len(ba) != packet_size: raise errors.InvalidPacketError() cid = ba[0:4] seq = ba[4] # We don't know the size limit a priori here without seeing the init # packet, so truncation needs to be done in the higher level protocol # handling code, unlike the degenerate case of a 1 packet message in an # init packet, where the size is known. payload = ba[5:] return UsbHidTransport.ContPacket(packet_size, cid, seq, payload) def __init__(self, hid_device, read_timeout_secs=3.0): self.hid_device = hid_device in_size = hid_device.GetInReportDataLength() out_size = hid_device.GetOutReportDataLength() if in_size != out_size: raise errors.HardwareError( 'unsupported device with different in/out packet sizes.') if in_size == 0: raise errors.HardwareError('unable to determine packet size') self.packet_size = in_size self.read_timeout_secs = read_timeout_secs self.logger = logging.getLogger('pyu2f.hidtransport') self.InternalInit() def SendMsgBytes(self, msg): r = self.InternalExchange(UsbHidTransport.U2FHID_MSG, msg) return r def SendBlink(self, length): return self.InternalExchange(UsbHidTransport.U2FHID_PROMPT, bytearray([length])) def SendWink(self): return self.InternalExchange(UsbHidTransport.U2FHID_WINK, bytearray([])) def SendPing(self, data): return self.InternalExchange(UsbHidTransport.U2FHID_PING, data) def InternalInit(self): """Initializes the device and obtains channel id.""" self.cid = UsbHidTransport.U2FHID_BROADCAST_CID nonce = bytearray(os.urandom(8)) r = self.InternalExchange(UsbHidTransport.U2FHID_INIT, nonce) if len(r) < 17: raise errors.HidError('unexpected init reply len') if r[0:8] != nonce: raise errors.HidError('nonce mismatch') self.cid = bytearray(r[8:12]) self.u2fhid_version = r[12] def InternalExchange(self, cmd, payload_in): """Sends and receives a message from the device.""" # make a copy because we destroy it below self.logger.debug('payload: ' + str(list(payload_in))) payload = bytearray() payload[:] = payload_in for _ in range(2): self.InternalSend(cmd, payload) ret_cmd, ret_payload = self.InternalRecv() if ret_cmd == UsbHidTransport.U2FHID_ERROR: if ret_payload == UsbHidTransport.ERR_CHANNEL_BUSY: time.sleep(0.5) continue raise errors.HidError('Device error: %d' % int(ret_payload[0])) elif ret_cmd != cmd: raise errors.HidError('Command mismatch!') return ret_payload raise errors.HidError('Device Busy. Please retry') def InternalSend(self, cmd, payload): """Sends a message to the device, including fragmenting it.""" length_to_send = len(payload) max_payload = self.packet_size - 7 first_frame = payload[0:max_payload] first_packet = UsbHidTransport.InitPacket(self.packet_size, self.cid, cmd, len(payload), first_frame) del payload[0:max_payload] length_to_send -= len(first_frame) self.InternalSendPacket(first_packet) seq = 0 while length_to_send > 0: max_payload = self.packet_size - 5 next_frame = payload[0:max_payload] del payload[0:max_payload] length_to_send -= len(next_frame) next_packet = UsbHidTransport.ContPacket(self.packet_size, self.cid, seq, next_frame) self.InternalSendPacket(next_packet) seq += 1 def InternalSendPacket(self, packet): wire = packet.ToWireFormat() self.logger.debug('sending packet: ' + str(wire)) self.hid_device.Write(wire) def InternalReadFrame(self): # TODO(gdasher): Figure out timeouts. Today, this implementation # blocks forever at the HID level waiting for a response to a report. # This may not be reasonable behavior (though in practice in seems to be # OK on the set of devices and machines tested so far). frame = self.hid_device.Read() self.logger.debug('recv: ' + str(frame)) return frame def InternalRecv(self): """Receives a message from the device, including defragmenting it.""" first_read = self.InternalReadFrame() first_packet = UsbHidTransport.InitPacket.FromWireFormat(self.packet_size, first_read) data = first_packet.payload to_read = first_packet.size - len(first_packet.payload) seq = 0 while to_read > 0: next_read = self.InternalReadFrame() next_packet = UsbHidTransport.ContPacket.FromWireFormat(self.packet_size, next_read) if self.cid != next_packet.cid: # Skip over packets that are for communication with other clients. # HID is broadcast, so we see potentially all communication from the # device. For well-behaved devices, these should be BUSY messages # sent to other clients of the device because at this point we're # in mid-message transit. continue if seq != next_packet.seq: raise errors.HardwareError('Packets received out of order') # This packet for us at this point, so debit it against our # balance of bytes to read. to_read -= len(next_packet.payload) data.extend(next_packet.payload) seq += 1 # truncate incomplete frames data = data[0:first_packet.size] return (first_packet.cmd, data) pyu2f-0.1.5/README.md0000664000372000037200000000305113747070544014715 0ustar travistravis00000000000000# pyu2f [![Build Status](https://travis-ci.org/google/pyu2f.svg?branch=master)](https://travis-ci.org/google/pyu2f) pyu2f is a python based U2F host library for Linux, Windows, and MacOS. It provides functionality for interacting with a U2F device over USB. ## Features pyu2f uses ctypes to make system calls directly to interface with the USB HID device. This means that no platform specific shared libraries need to be compiled for pyu2f to work. By default pyu2f will use its own U2F stack implementation to sign requests. If desired, pyu2f can offload signing to a pluggable command line tool. Offloading is not yet supported for U2F registration. ## Usage The recommended approach for U2F signing (authentication) is through the convenience interface: ``` from pyu2f import model from pyu2f.convenience import authenticator ... registered_key = model.RegisteredKey(b64_encoded_key) challenge_data = [{'key': registered_key, 'challenge': raw_challenge_data}] api = authenticator.CreateCompositeAuthenticator(origin) response = api.Authenticate(app_id, challenge_data) ``` See baseauthenticator.py for interface details. ## Authentication Plugin The convenience interface allows for a pluggable authenticator to be defined and used instead of the built in U2F stack. This can be done by setting the SK_SIGNING_PLUGIN environment variable to the plugin tool. The plugin tool should follow the specification detailed in customauthenticator.py If SK_SIGNING_PLUGIN is set, the convenience layer will invoke the signing plugin whenver Authenticate() is called. pyu2f-0.1.5/setup.cfg0000664000372000037200000000011213747070572015253 0ustar travistravis00000000000000[metadata] license_files = LICENSE [egg_info] tag_build = tag_date = 0 pyu2f-0.1.5/LICENSE0000664000372000037200000002613613747070544014454 0ustar travistravis00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. pyu2f-0.1.5/setup.py0000664000372000037200000000410613747070544015152 0ustar travistravis00000000000000#!/usr/bin/env python # # Copyright 2013 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. """Setup configuration.""" try: import setuptools except ImportError: from ez_setup import use_setuptools use_setuptools() import setuptools setuptools.setup( name='pyu2f', version='0.1.5', description='U2F host library for interacting with a U2F device over USB.', long_description='pyu2f is a python based U2F host library for Linux, ' 'Windows, and MacOS. It provides functionality for ' 'interacting with a U2F device over USB.', url='https://github.com/google/pyu2f/', author='Google Inc.', author_email='pyu2f-team@google.com', # Contained modules and scripts. packages=setuptools.find_packages(exclude=["pyu2f.tests", "pyu2f.tests.*"]), install_requires=[ 'six', ], tests_require=[ 'unittest2>=0.5.1', 'pyfakefs>=2.4', 'mock>=1.0.1', ], include_package_data=True, platforms=["Windows", "Linux", "OS X", "macOS"], # PyPI package information. classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', ], license='Apache 2.0', zip_safe=True, )