feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
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.

View File

@@ -0,0 +1,15 @@
# 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."""

View File

@@ -0,0 +1,147 @@
# 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)

View File

@@ -0,0 +1,15 @@
# 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 classes for U2F signing/authentication flow."""

View File

@@ -0,0 +1,53 @@
# 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.
"""Interface to handle end to end flow of U2F signing."""
import sys
from pyu2f.convenience import baseauthenticator
from pyu2f.convenience import customauthenticator
from pyu2f.convenience import localauthenticator
def CreateCompositeAuthenticator(origin):
authenticators = [customauthenticator.CustomAuthenticator(origin),
localauthenticator.LocalAuthenticator(origin)]
return CompositeAuthenticator(authenticators)
class CompositeAuthenticator(baseauthenticator.BaseAuthenticator):
"""Composes multiple authenticators into a single authenticator.
Priority is based on the order of the list initialized with the instance.
"""
def __init__(self, authenticators):
self.authenticators = authenticators
def Authenticate(self, app_id, challenge_data,
print_callback=sys.stderr.write):
"""See base class."""
for authenticator in self.authenticators:
if authenticator.IsAvailable():
result = authenticator.Authenticate(app_id,
challenge_data,
print_callback)
return result
raise ValueError('No valid authenticators found')
def IsAvailable(self):
"""See base class."""
return True

View File

@@ -0,0 +1,58 @@
# 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.
"""Interface to handle end to end flow of U2F signing."""
import sys
class BaseAuthenticator(object):
"""Interface to handle end to end flow of U2F signing."""
def Authenticate(self, app_id, challenge_data,
print_callback=sys.stderr.write):
"""Authenticates app_id with a security key.
Executes the U2F authentication/signature flow with a security key.
Args:
app_id: The app_id to register the security key against.
challenge_data: List of dictionaries containing a RegisteredKey ('key')
and the raw challenge data ('challenge') for this key.
print_callback: Callback to print a message to the user. The callback
function takes one argument--the message to display.
Returns:
A dictionary with the following fields:
'clientData': url-safe base64 encoded ClientData JSON signed by the key.
'signatureData': url-safe base64 encoded signature.
'applicationId': application id.
'keyHandle': url-safe base64 encoded handle of the key used to sign.
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).
"""
raise NotImplementedError
def IsAvailable(self):
"""Indicates whether the authenticator implementation is available to sign.
The caller should not call Authenticate() if IsAvailable() returns False
Returns:
True if the authenticator is available to sign and False otherwise.
"""
raise NotImplementedError

View File

@@ -0,0 +1,245 @@
# 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": <url-safe base64-encoded key handle>,
"appIdHash": <url-safe base64-encoded SHA-256 hash of application ID>,
"challengeHash": <url-safe base64-encoded SHA-256 hash of ClientData>,
"version": U2F protocol version (usually "U2F_V2")
},...],
"timeoutSeconds": <security key touch timeout>
}
Signing Response JSON
{
"type": "sign_helper_reply",
"code": <result code>.
"errorDetail": <text description of error>,
"responseData": {
"appIdHash": <url-safe base64-encoded SHA-256 hash of application ID>,
"challengeHash": <url-safe base64-encoded SHA-256 hash of ClientData>,
"keyHandle": <url-safe base64-encoded key handle>,
"version": <U2F protocol version>,
"signatureData": <url-safe base64-encoded signature>
}
}
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('<I', input_length)
request = length_bytes_le + input_json.encode()
# Call plugin
sign_process = subprocess.Popen(cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = sign_process.communicate(request)[0]
exit_status = sign_process.wait()
# Parse and validate response size
response_len_le = stdout[:4]
response_len = struct.unpack('<I', response_len_le)[0]
response = stdout[4:]
if response_len != len(response):
raise errors.PluginError(
'Plugin response length {} does not match data {} (exit_status={})'
.format(response_len, len(response), exit_status))
# Ensure valid json
try:
json_response = json.loads(response.decode())
except ValueError:
raise errors.PluginError('Plugin returned invalid output (exit_status={})'
.format(exit_status))
# Ensure response type
if json_response.get('type') != 'sign_helper_reply':
error_string = 'Plugin returned invalid response type (exit_status={})'.format(exit_status)
error_detail = json_response.get('errorDetail')
if (error_detail):
error_string += '. Additional information:\n' + error_detail
raise errors.PluginError(error_string)
# Parse response codes
result_code = json_response.get('code')
if result_code is None:
raise errors.PluginError('Plugin missing result code (exit_status={})'
.format(exit_status))
# Handle errors
if result_code == SK_SIGNING_PLUGIN_TOUCH_REQUIRED:
raise errors.U2FError(errors.U2FError.TIMEOUT)
elif result_code == SK_SIGNING_PLUGIN_WRONG_DATA:
raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE)
elif result_code != SK_SIGNING_PLUGIN_NO_ERROR:
raise errors.PluginError(
'Plugin failed with error {} - {} (exit_status={})'
.format(result_code,
json_response.get('errorDetail'),
exit_status))
# Ensure response data is present
response_data = json_response.get('responseData')
if response_data is None:
raise errors.PluginErrors(
'Plugin returned output with missing responseData (exit_status={})'
.format(exit_status))
return response_data
def _SHA256(self, string):
"""Helper method to perform SHA256."""
md = hashlib.sha256()
md.update(string.encode())
return md.digest()
def _Base64Encode(self, bytes_data):
"""Helper method to base64 encode, strip padding, and return str
result."""
return base64.urlsafe_b64encode(bytes_data).decode().rstrip('=')

View File

@@ -0,0 +1,75 @@
# 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()

View File

@@ -0,0 +1,99 @@
# 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

View File

@@ -0,0 +1,170 @@
# 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

View File

@@ -0,0 +1,50 @@
# 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)

View File

@@ -0,0 +1,101 @@
# 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

View File

@@ -0,0 +1,235 @@
# 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 interface on Linux using SysFS and device files."""
from __future__ import division
import os
import struct
from pyu2f import errors
from pyu2f.hid import base
REPORT_DESCRIPTOR_KEY_MASK = 0xfc
LONG_ITEM_ENCODING = 0xfe
OUTPUT_ITEM = 0x90
INPUT_ITEM = 0x80
COLLECTION_ITEM = 0xa0
REPORT_COUNT = 0x94
REPORT_SIZE = 0x74
USAGE_PAGE = 0x04
USAGE = 0x08
def GetValueLength(rd, pos):
"""Get value length for a key in rd.
For a key at position pos in the Report Descriptor rd, return the length
of the associated value. This supports both short and long format
values.
Args:
rd: Report Descriptor
pos: The position of the key in rd.
Returns:
(key_size, data_len) where key_size is the number of bytes occupied by
the key and data_len is the length of the value associated by the key.
"""
rd = bytearray(rd)
key = rd[pos]
if key == LONG_ITEM_ENCODING:
# If the key is tagged as a long item (0xfe), then the format is
# [key (1 byte)] [data len (1 byte)] [item tag (1 byte)] [data (n # bytes)].
# Thus, the entire key record is 3 bytes long.
if pos + 1 < len(rd):
return (3, rd[pos + 1])
else:
raise errors.HidError('Malformed report descriptor')
else:
# If the key is tagged as a short item, then the item tag and data len are
# packed into one byte. The format is thus:
# [tag (high 4 bits)] [type (2 bits)] [size code (2 bits)] [data (n bytes)].
# The size code specifies 1,2, or 4 bytes (0x03 means 4 bytes).
code = key & 0x03
if code <= 0x02:
return (1, code)
elif code == 0x03:
return (1, 4)
raise errors.HidError('Cannot happen')
def ReadLsbBytes(rd, offset, value_size):
"""Reads value_size bytes from rd at offset, least signifcant byte first."""
encoding = None
if value_size == 1:
encoding = '<B'
elif value_size == 2:
encoding = '<H'
elif value_size == 4:
encoding = '<L'
else:
raise errors.HidError('Invalid value size specified')
ret, = struct.unpack(encoding, rd[offset:offset + value_size])
return ret
class NoReportCountFound(Exception):
pass
def ParseReportDescriptor(rd, desc):
"""Parse the binary report descriptor.
Parse the binary report descriptor into a DeviceDescriptor object.
Args:
rd: The binary report descriptor
desc: The DeviceDescriptor object to update with the results
from parsing the descriptor.
Returns:
None
"""
rd = bytearray(rd)
pos = 0
report_count = None
report_size = None
usage_page = None
usage = None
while pos < len(rd):
key = rd[pos]
# First step, determine the value encoding (either long or short).
key_size, value_length = GetValueLength(rd, pos)
if key & REPORT_DESCRIPTOR_KEY_MASK == INPUT_ITEM:
if report_count and report_size:
byte_length = (report_count * report_size) // 8
desc.internal_max_in_report_len = max(
desc.internal_max_in_report_len, byte_length)
report_count = None
report_size = None
elif key & REPORT_DESCRIPTOR_KEY_MASK == OUTPUT_ITEM:
if report_count and report_size:
byte_length = (report_count * report_size) // 8
desc.internal_max_out_report_len = max(
desc.internal_max_out_report_len, byte_length)
report_count = None
report_size = None
elif key & REPORT_DESCRIPTOR_KEY_MASK == COLLECTION_ITEM:
if usage_page:
desc.usage_page = usage_page
if usage:
desc.usage = usage
elif key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_COUNT:
if len(rd) >= 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():
hidraw_devices = []
try:
hidraw_devices = os.listdir('/sys/class/hidraw')
except FileNotFoundError:
raise errors.OsHidError('No hidraw device is available')
for dev in hidraw_devices:
rd_path = (
os.path.join(
'/sys/class/hidraw', dev,
'device/report_descriptor'))
uevent_path = os.path.join('/sys/class/hidraw', dev, 'device/uevent')
rd_file = open(rd_path, 'rb')
uevent_file = open(uevent_path, 'rb')
desc = base.DeviceDescriptor()
desc.path = os.path.join('/dev/', dev)
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

View File

@@ -0,0 +1,458 @@
# 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.warning('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()

View File

@@ -0,0 +1,375 @@
# 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())
except WindowsError as e:
continue # skip this 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.")
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)

View File

@@ -0,0 +1,331 @@
# 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(user): 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)

View File

@@ -0,0 +1,80 @@
# 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

View File

@@ -0,0 +1,183 @@
# 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(user): 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()