Import Upstream version 1.3.3
This commit is contained in:
44
yubico/__init__.py
Normal file
44
yubico/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
the yubico package
|
||||
|
||||
See http://www.yubico.com/yubikey/ for information about the YubiKey.
|
||||
|
||||
Example usage :
|
||||
|
||||
import yubico
|
||||
|
||||
try:
|
||||
YK = yubico.find_yubikey(debug=True)
|
||||
print "Version : %s " % YK.version()
|
||||
except yubico.yubico_exception.YubicoError as e:
|
||||
print "ERROR: %s" % e.reason
|
||||
sys.exit(1)
|
||||
|
||||
To learn about configuring your YubiKey using this framework, see the
|
||||
yubikey_config module.
|
||||
"""
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
from .yubico_version import __version__
|
||||
|
||||
__all__ = [
|
||||
# classes
|
||||
'YubiKey',
|
||||
# functions
|
||||
"find_yubikey",
|
||||
# modules
|
||||
"yubico_exception",
|
||||
"yubico_util",
|
||||
"yubikey",
|
||||
"yubikey_config",
|
||||
"yubikey_config_util",
|
||||
"yubikey_defs",
|
||||
"yubikey_frame",
|
||||
"yubikey_usb_hid",
|
||||
"yubikey_neo_usb_hid",
|
||||
]
|
||||
|
||||
# to not have to import yubico.yubikey
|
||||
from .yubikey import YubiKey
|
||||
from .yubikey import find_key as find_yubikey
|
||||
54
yubico/yubico_exception.py
Normal file
54
yubico/yubico_exception.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
class for exceptions used in the other Yubico modules
|
||||
|
||||
All exceptions raised by the different Yubico modules are inherited
|
||||
from the base class YubicoError. That means you can trap them all,
|
||||
without knowing the details, with code like this :
|
||||
|
||||
try:
|
||||
# something Yubico related
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print "ERROR: %s" % inst.reason
|
||||
"""
|
||||
# Copyright (c) 2010, Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
# classes
|
||||
'YubicoError',
|
||||
'InputError',
|
||||
'YubiKeyTimeout',
|
||||
]
|
||||
|
||||
from .yubico_version import __version__
|
||||
|
||||
|
||||
class YubicoError(Exception):
|
||||
"""
|
||||
Base class for Yubico exceptions in the yubico package.
|
||||
|
||||
Attributes:
|
||||
reason -- explanation of the error
|
||||
"""
|
||||
|
||||
def __init__(self, reason):
|
||||
self.reason = reason
|
||||
|
||||
def __str__(self):
|
||||
return '<%s instance at %s: %s>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.reason
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InputError(YubicoError):
|
||||
"""
|
||||
Exception raised for errors in an input to some function.
|
||||
"""
|
||||
def __init__(self, reason='input validation error'):
|
||||
super(InputError, self).__init__(reason)
|
||||
158
yubico/yubico_util.py
Normal file
158
yubico/yubico_util.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
utility functions for Yubico modules
|
||||
"""
|
||||
# Copyright (c) 2010, Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
'crc16',
|
||||
'validate_crc16',
|
||||
'hexdump',
|
||||
'modhex_decode',
|
||||
'hotp_truncate',
|
||||
# classes
|
||||
]
|
||||
|
||||
import sys
|
||||
import string
|
||||
|
||||
from .yubico_version import __version__
|
||||
from . import yubikey_defs
|
||||
from . import yubico_exception
|
||||
|
||||
_CRC_OK_RESIDUAL = 0xf0b8
|
||||
|
||||
def ord_byte(byte):
|
||||
"""Convert a byte to its integer value"""
|
||||
if sys.version_info < (3, 0):
|
||||
return ord(byte)
|
||||
else:
|
||||
# In Python 3, single bytes are represented as integers
|
||||
return int(byte)
|
||||
|
||||
def chr_byte(number):
|
||||
"""Convert an integer value to a length-1 bytestring"""
|
||||
if sys.version_info < (3, 0):
|
||||
return chr(number)
|
||||
else:
|
||||
return bytes([number])
|
||||
|
||||
def crc16(data):
|
||||
"""
|
||||
Calculate an ISO13239 CRC checksum of the input buffer (bytestring).
|
||||
"""
|
||||
m_crc = 0xffff
|
||||
for this in data:
|
||||
m_crc ^= ord_byte(this)
|
||||
for _ in range(8):
|
||||
j = m_crc & 1
|
||||
m_crc >>= 1
|
||||
if j:
|
||||
m_crc ^= 0x8408
|
||||
return m_crc
|
||||
|
||||
def validate_crc16(data):
|
||||
"""
|
||||
Validate that the CRC of the contents of buffer is the residual OK value.
|
||||
|
||||
The input is a bytestring.
|
||||
"""
|
||||
return crc16(data) == _CRC_OK_RESIDUAL
|
||||
|
||||
|
||||
class DumpColors:
|
||||
""" Class holding ANSI colors for colorization of hexdump output """
|
||||
|
||||
def __init__(self):
|
||||
self.colors = {'BLUE': '\033[94m',
|
||||
'GREEN': '\033[92m',
|
||||
'RESET': '\033[0m',
|
||||
}
|
||||
self.enabled = True
|
||||
return None
|
||||
|
||||
def get(self, what):
|
||||
"""
|
||||
Get the ANSI code for 'what'
|
||||
|
||||
Returns an empty string if disabled/not found
|
||||
"""
|
||||
if self.enabled:
|
||||
if what in self.colors:
|
||||
return self.colors[what]
|
||||
return ''
|
||||
|
||||
def enable(self):
|
||||
""" Enable colorization """
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
""" Disable colorization """
|
||||
self.enabled = False
|
||||
|
||||
def hexdump(src, length=8, colorize=False):
|
||||
""" Produce a string hexdump of src, for debug output.
|
||||
|
||||
Input: bytestring; output: text string
|
||||
"""
|
||||
if not src:
|
||||
return str(src)
|
||||
if type(src) is not bytes:
|
||||
raise yubico_exception.InputError('Hexdump \'src\' must be bytestring (got %s)' % type(src))
|
||||
offset = 0
|
||||
result = ''
|
||||
for this in group(src, length):
|
||||
if colorize:
|
||||
last, this = this[-1], this[:-1]
|
||||
colors = DumpColors()
|
||||
color = colors.get('RESET')
|
||||
if ord_byte(last) & yubikey_defs.RESP_PENDING_FLAG:
|
||||
# write to key
|
||||
color = colors.get('BLUE')
|
||||
elif ord_byte(last) & yubikey_defs.SLOT_WRITE_FLAG:
|
||||
color = colors.get('GREEN')
|
||||
hex_s = color + ' '.join(["%02x" % ord_byte(x) for x in this]) + colors.get('RESET')
|
||||
hex_s += " %02x" % ord_byte(last)
|
||||
else:
|
||||
hex_s = ' '.join(["%02x" % ord_byte(x) for x in this])
|
||||
result += "%04X %s\n" % (offset, hex_s)
|
||||
offset += length
|
||||
return result
|
||||
|
||||
def group(data, num):
|
||||
""" Split data into chunks of num chars each """
|
||||
return [data[i:i+num] for i in range(0, len(data), num)]
|
||||
|
||||
def modhex_decode(data):
|
||||
""" Convert a modhex bytestring to ordinary hex. """
|
||||
try:
|
||||
maketrans = string.maketrans
|
||||
except AttributeError:
|
||||
# Python 3
|
||||
maketrans = bytes.maketrans
|
||||
t_map = maketrans(b"cbdefghijklnrtuv", b"0123456789abcdef")
|
||||
return data.translate(t_map)
|
||||
|
||||
def hotp_truncate(hmac_result, length=6):
|
||||
""" Perform the HOTP Algorithm truncating.
|
||||
|
||||
Input is a bytestring.
|
||||
"""
|
||||
if len(hmac_result) != 20:
|
||||
raise yubico_exception.YubicoError("HMAC-SHA-1 not 20 bytes long")
|
||||
offset = ord_byte(hmac_result[19]) & 0xf
|
||||
bin_code = (ord_byte(hmac_result[offset]) & 0x7f) << 24 \
|
||||
| (ord_byte(hmac_result[offset+1]) & 0xff) << 16 \
|
||||
| (ord_byte(hmac_result[offset+2]) & 0xff) << 8 \
|
||||
| (ord_byte(hmac_result[offset+3]) & 0xff)
|
||||
return bin_code % (10 ** length)
|
||||
|
||||
def tlv_parse(data):
|
||||
""" Parses a bytestring of TLV values into a dict with the tags as keys."""
|
||||
parsed = {}
|
||||
while data:
|
||||
t, l, data = ord_byte(data[0]), ord_byte(data[1]), data[2:]
|
||||
parsed[t], data = data[:l], data[l:]
|
||||
return parsed
|
||||
1
yubico/yubico_version.py
Normal file
1
yubico/yubico_version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "1.3.3"
|
||||
67
yubico/yubikey.py
Normal file
67
yubico/yubikey.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
module for accessing a YubiKey
|
||||
|
||||
In an attempt to support any future versions of the YubiKey which
|
||||
might not be USB HID devices, you should always use the yubikey.find_key()
|
||||
(or better yet, yubico.find_yubikey()) function to initialize
|
||||
communication with YubiKeys.
|
||||
|
||||
Example usage (if using this module directly, see base module yubico) :
|
||||
|
||||
import yubico.yubikey
|
||||
|
||||
try:
|
||||
YK = yubico.yubikey.find_key()
|
||||
print "Version : %s " % YK.version()
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print "ERROR: %s" % inst.reason
|
||||
"""
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
'RESP_TIMEOUT_WAIT_FLAG',
|
||||
'RESP_PENDING_FLAG',
|
||||
'SLOT_WRITE_FLAG',
|
||||
# functions
|
||||
'find_key',
|
||||
# classes
|
||||
'YubiKey',
|
||||
'YubiKeyTimeout',
|
||||
]
|
||||
|
||||
from .yubico_version import __version__
|
||||
from .yubikey_base import YubiKeyError, YubiKeyTimeout, YubiKeyVersionError, YubiKeyCapabilities, YubiKey
|
||||
from .yubikey_usb_hid import YubiKeyUSBHID, YubiKeyHIDDevice, YubiKeyUSBHIDError
|
||||
from .yubikey_neo_usb_hid import YubiKeyNEO_USBHID
|
||||
from .yubikey_4_usb_hid import YubiKey4_USBHID
|
||||
|
||||
|
||||
def find_key(debug=False, skip=0):
|
||||
"""
|
||||
Locate a connected YubiKey. Throws an exception if none is found.
|
||||
|
||||
This function is supposed to be possible to extend if any other YubiKeys
|
||||
appear in the future.
|
||||
|
||||
Attributes :
|
||||
skip -- number of YubiKeys to skip
|
||||
debug -- True or False
|
||||
"""
|
||||
try:
|
||||
hid_device = YubiKeyHIDDevice(debug, skip)
|
||||
yk_version = hid_device.status().ykver()
|
||||
if (2, 1, 4) <= yk_version <= (2, 1, 9):
|
||||
return YubiKeyNEO_USBHID(debug, skip, hid_device)
|
||||
if yk_version < (3, 0, 0):
|
||||
return YubiKeyUSBHID(debug, skip, hid_device)
|
||||
if yk_version < (4, 0, 0):
|
||||
return YubiKeyNEO_USBHID(debug, skip, hid_device)
|
||||
return YubiKey4_USBHID(debug, skip, hid_device)
|
||||
except YubiKeyUSBHIDError as inst:
|
||||
if 'No USB YubiKey found' in str(inst):
|
||||
# generalize this error
|
||||
raise YubiKeyError('No YubiKey found')
|
||||
else:
|
||||
raise
|
||||
113
yubico/yubikey_4_usb_hid.py
Normal file
113
yubico/yubikey_4_usb_hid.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
module for accessing a USB HID YubiKey 4
|
||||
"""
|
||||
|
||||
# Copyright (c) 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
# classes
|
||||
'YubiKey4_USBHID',
|
||||
'YubiKey4_USBHIDError'
|
||||
]
|
||||
|
||||
from .yubikey_defs import SLOT, MODE, YK4_CAPA
|
||||
from . import yubikey_frame
|
||||
from . import yubikey_base
|
||||
from . import yubico_exception
|
||||
from . import yubico_util
|
||||
from . import yubikey_neo_usb_hid
|
||||
|
||||
MODE_CAPABILITIES = { # Required capabilities to support USB mode.
|
||||
MODE.OTP : [YK4_CAPA.OTP],
|
||||
MODE.CCID : [YK4_CAPA.CCID],
|
||||
MODE.OTP_CCID : [YK4_CAPA.OTP, YK4_CAPA.CCID],
|
||||
MODE.U2F : [YK4_CAPA.U2F],
|
||||
MODE.OTP_U2F : [YK4_CAPA.OTP, YK4_CAPA.U2F],
|
||||
MODE.U2F_CCID : [YK4_CAPA.U2F, YK4_CAPA.CCID],
|
||||
MODE.OTP_U2F_CCID : [YK4_CAPA.OTP, YK4_CAPA.U2F, YK4_CAPA.CCID]
|
||||
}
|
||||
|
||||
|
||||
class YubiKey4_USBHIDError(yubico_exception.YubicoError):
|
||||
""" Exception raised for errors with the YK4 USB HID communication. """
|
||||
|
||||
|
||||
class YubiKey4_USBHIDCapabilities(yubikey_neo_usb_hid.YubiKeyNEO_USBHIDCapabilities):
|
||||
"""
|
||||
Capabilities of current YubiKey 4.
|
||||
"""
|
||||
_yk4_capa = 0
|
||||
|
||||
def _set_yk4_capa(self, yk4_capa):
|
||||
int_val = 0
|
||||
for b in yk4_capa:
|
||||
int_val <<= 8
|
||||
int_val += yubico_util.ord_byte(b)
|
||||
self._yk4_capa = int_val
|
||||
|
||||
def have_nfc_ndef(self, slot=1):
|
||||
return False
|
||||
|
||||
def have_usb_mode(self, mode):
|
||||
mode &= ~MODE.FLAG_EJECT # Mask away eject flag
|
||||
if self.version < (4, 1, 0): # YK Plus is locked in OTP+U2F
|
||||
return mode == MODE.OTP_U2F
|
||||
for cap_req in MODE_CAPABILITIES.get(mode, [0]):
|
||||
if not self.have_capability(cap_req):
|
||||
return False
|
||||
return True
|
||||
|
||||
def have_capabilities(self):
|
||||
return self.version >= (4, 1, 0)
|
||||
|
||||
def have_capability(self, capability):
|
||||
return self._yk4_capa & capability != 0
|
||||
|
||||
|
||||
class YubiKey4_USBHID(yubikey_neo_usb_hid.YubiKeyNEO_USBHID):
|
||||
"""
|
||||
Class for accessing a YubiKey 4 over USB HID.
|
||||
|
||||
"""
|
||||
|
||||
model = 'YubiKey 4'
|
||||
description = 'YubiKey 4'
|
||||
_capabilities_cls = YubiKey4_USBHIDCapabilities
|
||||
|
||||
def __init__(self, debug=False, skip=0, hid_device=None):
|
||||
"""
|
||||
Find and connect to a YubiKey 4 (USB HID).
|
||||
|
||||
Attributes :
|
||||
skip -- number of YubiKeys to skip
|
||||
debug -- True or False
|
||||
"""
|
||||
super(YubiKey4_USBHID, self).__init__(debug, skip, hid_device)
|
||||
if self.version_num() < (4, 0, 0):
|
||||
raise yubikey_base.YubiKeyVersionError(
|
||||
"Incorrect version for YubiKey 4 %s" % self.version())
|
||||
elif self.version_num() < (4, 1, 0):
|
||||
self.description = 'YubiKey Plus'
|
||||
elif self.version_num() < (4, 2, 0):
|
||||
self.description = 'YubiKey Edge/Edge-n'
|
||||
|
||||
if self.capabilities.have_capabilities():
|
||||
data = yubico_util.tlv_parse(self._read_capabilities())
|
||||
self.capabilities._set_yk4_capa(data.get(YK4_CAPA.TAG.CAPA, b''))
|
||||
|
||||
def _read_capabilities(self):
|
||||
""" Read the capabilities list from a YubiKey >= 4.0.0 """
|
||||
|
||||
frame = yubikey_frame.YubiKeyFrame(command=SLOT.YK4_CAPABILITIES)
|
||||
self._device._write(frame)
|
||||
response = self._device._read_response()
|
||||
r_len = yubico_util.ord_byte(response[0])
|
||||
|
||||
# 1 byte length, 2 byte CRC.
|
||||
if not yubico_util.validate_crc16(response[:r_len+3]):
|
||||
raise YubiKey4_USBHIDError("Read from device failed CRC check")
|
||||
|
||||
return response[1:r_len+1]
|
||||
198
yubico/yubikey_base.py
Normal file
198
yubico/yubikey_base.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
module for Yubikey base classes
|
||||
"""
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
from .yubico_version import __version__
|
||||
from . import yubico_exception
|
||||
|
||||
class YubiKeyError(yubico_exception.YubicoError):
|
||||
"""
|
||||
Exception raised concerning YubiKey operations.
|
||||
|
||||
Attributes:
|
||||
reason -- explanation of the error
|
||||
"""
|
||||
def __init__(self, reason='no details'):
|
||||
super(YubiKeyError, self).__init__(reason)
|
||||
|
||||
class YubiKeyTimeout(YubiKeyError):
|
||||
"""
|
||||
Exception raised when a YubiKey operation timed out.
|
||||
|
||||
Attributes:
|
||||
reason -- explanation of the error
|
||||
"""
|
||||
def __init__(self, reason='no details'):
|
||||
super(YubiKeyTimeout, self).__init__(reason)
|
||||
|
||||
class YubiKeyVersionError(YubiKeyError):
|
||||
"""
|
||||
Exception raised when the YubiKey is not capable of something requested.
|
||||
|
||||
Attributes:
|
||||
reason -- explanation of the error
|
||||
"""
|
||||
def __init__(self, reason='no details'):
|
||||
super(YubiKeyVersionError, self).__init__(reason)
|
||||
|
||||
|
||||
class YubiKeyCapabilities(object):
|
||||
"""
|
||||
Class expressing the functionality of a YubiKey.
|
||||
|
||||
This base class should be the superset of all sub-classes.
|
||||
|
||||
In this base class, we lie and say 'yes' to all capabilities.
|
||||
|
||||
If the base class is used (such as when creating a YubiKeyConfig()
|
||||
before getting a YubiKey()), errors must be handled at runtime
|
||||
(or later, when the user is unable to use the YubiKey).
|
||||
"""
|
||||
|
||||
model = 'Unknown'
|
||||
version = (0, 0, 0,)
|
||||
version_num = 0x0
|
||||
default_answer = True
|
||||
|
||||
def __init__(self, model = None, version = None, default_answer = None):
|
||||
self.model = model
|
||||
if default_answer is not None:
|
||||
self.default_answer = default_answer
|
||||
if version is not None:
|
||||
self.version = version
|
||||
(major, minor, build,) = version
|
||||
# convert 2.1.3 to 0x00020103
|
||||
self.version_num = (major << 24) | (minor << 16) | build
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s instance at %s: Device %s %s (default: %s)>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.model,
|
||||
self.version,
|
||||
self.default_answer,
|
||||
)
|
||||
|
||||
def have_yubico_OTP(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_OATH(self, mode):
|
||||
return self.default_answer
|
||||
|
||||
def have_challenge_response(self, mode):
|
||||
return self.default_answer
|
||||
|
||||
def have_serial_number(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_ticket_flag(self, flag):
|
||||
return self.default_answer
|
||||
|
||||
def have_config_flag(self, flag):
|
||||
return self.default_answer
|
||||
|
||||
def have_extended_flag(self, flag):
|
||||
return self.default_answer
|
||||
|
||||
def have_extended_scan_code_mode(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_shifted_1_mode(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_nfc_ndef(self, slot=1):
|
||||
return self.default_answer
|
||||
|
||||
def have_configuration_slot(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_device_config(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_usb_mode(self, mode):
|
||||
return self.default_answer
|
||||
|
||||
def have_scanmap(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_capabilities(self):
|
||||
return self.default_answer
|
||||
|
||||
def have_capability(self, capability):
|
||||
return self.default_answer
|
||||
|
||||
|
||||
class YubiKey(object):
|
||||
"""
|
||||
Base class for accessing YubiKeys
|
||||
"""
|
||||
|
||||
debug = None
|
||||
capabilities = None
|
||||
|
||||
def __init__(self, debug, capabilities = None):
|
||||
self.debug = debug
|
||||
if capabilities is None:
|
||||
self.capabilities = YubiKeyCapabilities(default_answer = False)
|
||||
else:
|
||||
self.capabilities = capabilities
|
||||
return None
|
||||
|
||||
def version(self):
|
||||
""" Get the connected YubiKey's version as a string. """
|
||||
pass
|
||||
|
||||
def serial(self, may_block=True):
|
||||
"""
|
||||
Get the connected YubiKey's serial number.
|
||||
|
||||
Note that since version 2.?.? this requires the YubiKey to be
|
||||
configured with the extended flag SERIAL_API_VISIBLE.
|
||||
|
||||
If the YubiKey is configured with SERIAL_BTN_VISIBLE set to True,
|
||||
it will start blinking and require a button press before revealing
|
||||
the serial number, with a 15 seconds timeout. Set `may_block'
|
||||
to False to abort if this is the case.
|
||||
"""
|
||||
pass
|
||||
|
||||
def challenge(self, challenge, mode='HMAC', slot=1, variable=True, may_block=True):
|
||||
"""
|
||||
Get the response to a challenge from a connected YubiKey.
|
||||
|
||||
`mode' is either 'HMAC' or 'OTP'.
|
||||
`slot' is 1 or 2.
|
||||
`variable' is only relevant for mode == HMAC.
|
||||
|
||||
If variable is True, challenge will be padded such that the
|
||||
YubiKey will compute the HMAC as if there were no padding.
|
||||
If variable is False, challenge will always be NULL-padded
|
||||
to 64 bytes.
|
||||
|
||||
The special case of no input will be HMACed by the YubiKey
|
||||
(in variable HMAC mode) as data = 0x00, length = 1.
|
||||
|
||||
In mode 'OTP', the challenge should be exactly 6 bytes. The
|
||||
response will be a YubiKey "ticket" with the 6-byte challenge
|
||||
in the ticket.uid field. The rest of the "ticket" will contain
|
||||
timestamp and counter information, so two identical challenges
|
||||
will NOT result in the same responses. The response is
|
||||
decryptable using AES ECB if you have access to the AES key
|
||||
programmed into the YubiKey.
|
||||
"""
|
||||
pass
|
||||
|
||||
def init_config(self):
|
||||
"""
|
||||
Return a YubiKey configuration object for this type of YubiKey.
|
||||
"""
|
||||
pass
|
||||
|
||||
def write_config(self, cfg, slot):
|
||||
"""
|
||||
Configure a YubiKey using a configuration object.
|
||||
"""
|
||||
pass
|
||||
557
yubico/yubikey_config.py
Normal file
557
yubico/yubikey_config.py
Normal file
@@ -0,0 +1,557 @@
|
||||
"""
|
||||
module for configuring YubiKeys
|
||||
"""
|
||||
# Copyright (c) 2010, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
'TicketFlags',
|
||||
'ConfigFlags',
|
||||
'ExtendedFlags',
|
||||
# functions
|
||||
# classes
|
||||
'YubiKeyConfigError',
|
||||
'YubiKeyConfig',
|
||||
]
|
||||
|
||||
from .yubico_version import __version__
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
from . import yubico_util
|
||||
from . import yubikey_defs
|
||||
from . import yubikey_frame
|
||||
from . import yubico_exception
|
||||
from . import yubikey_base
|
||||
from .yubikey_config_util import YubiKeyConfigBits, YubiKeyConfigFlag, YubiKeyExtendedFlag, YubiKeyTicketFlag
|
||||
from .yubikey_defs import SLOT
|
||||
|
||||
|
||||
def command2str(num):
|
||||
""" Turn command number into name """
|
||||
for attr in SLOT.__dict__.keys():
|
||||
if not attr.startswith('_') and attr == attr.upper():
|
||||
if getattr(SLOT, attr) == num:
|
||||
return 'SLOT_%s' % attr
|
||||
|
||||
return "0x%02x" % (num)
|
||||
|
||||
### BEGIN DEPRECATED
|
||||
### These are here for backwards compatibility, DO NOT USE!
|
||||
SLOT_CONFIG = SLOT.CONFIG
|
||||
SLOT_CONFIG2 = SLOT.CONFIG2
|
||||
SLOT_UPDATE1 = SLOT.UPDATE1
|
||||
SLOT_UPDATE2 = SLOT.UPDATE2
|
||||
SLOT_SWAP = SLOT.SWAP
|
||||
### END DEPRECATED
|
||||
|
||||
|
||||
TicketFlags = [
|
||||
YubiKeyTicketFlag('TAB_FIRST', 0x01, min_ykver=(1, 0), doc='Send TAB before first part'),
|
||||
YubiKeyTicketFlag('APPEND_TAB1', 0x02, min_ykver=(1, 0), doc='Send TAB after first part'),
|
||||
YubiKeyTicketFlag('APPEND_TAB2', 0x04, min_ykver=(1, 0), doc='Send TAB after second part'),
|
||||
YubiKeyTicketFlag('APPEND_DELAY1', 0x08, min_ykver=(1, 0), doc='Add 0.5s delay after first part'),
|
||||
YubiKeyTicketFlag('APPEND_DELAY2', 0x10, min_ykver=(1, 0), doc='Add 0.5s delay after second part'),
|
||||
YubiKeyTicketFlag('APPEND_CR', 0x20, min_ykver=(1, 0), doc='Append CR as final character'),
|
||||
YubiKeyTicketFlag('OATH_HOTP', 0x40, min_ykver=(2, 1), doc='Choose OATH-HOTP mode'),
|
||||
YubiKeyTicketFlag('CHAL_RESP', 0x40, min_ykver=(2, 2), doc='Choose Challenge-Response mode'),
|
||||
YubiKeyTicketFlag('PROTECT_CFG2', 0x80, min_ykver=(2, 0), doc='Protect configuration in slot 2'),
|
||||
]
|
||||
|
||||
ConfigFlags = [
|
||||
YubiKeyConfigFlag('SEND_REF', 0x01, min_ykver=(1, 0), doc='Send reference string (0..F) before data'),
|
||||
YubiKeyConfigFlag('TICKET_FIRST', 0x02, min_ykver=(1, 0), doc='Send ticket first (default is fixed part)', max_ykver=(1, 9)),
|
||||
YubiKeyConfigFlag('PACING_10MS', 0x04, min_ykver=(1, 0), doc='Add 10ms intra-key pacing'),
|
||||
YubiKeyConfigFlag('PACING_20MS', 0x08, min_ykver=(1, 0), doc='Add 20ms intra-key pacing'),
|
||||
#YubiKeyConfigFlag('ALLOW_HIDTRIG', 0x10, min_ykver=(1, 0), doc='DONT USE: Allow trigger through HID/keyboard', max_ykver=(1, 9)),
|
||||
YubiKeyConfigFlag('STATIC_TICKET', 0x20, min_ykver=(1, 0), doc='Static ticket generation'),
|
||||
|
||||
# YubiKey 2.0 and above
|
||||
YubiKeyConfigFlag('SHORT_TICKET', 0x02, min_ykver=(2, 0), doc='Send truncated ticket (half length)'),
|
||||
YubiKeyConfigFlag('STRONG_PW1', 0x10, min_ykver=(2, 0), doc='Strong password policy flag #1 (mixed case)'),
|
||||
YubiKeyConfigFlag('STRONG_PW2', 0x40, min_ykver=(2, 0), doc='Strong password policy flag #2 (subtitute 0..7 to digits)'),
|
||||
YubiKeyConfigFlag('MAN_UPDATE', 0x80, min_ykver=(2, 0), doc='Allow manual (local) update of static OTP'),
|
||||
|
||||
# YubiKey 2.1 and above
|
||||
YubiKeyConfigFlag('OATH_HOTP8', 0x02, min_ykver=(2, 1), mode='OATH', doc='Generate 8 digits HOTP rather than 6 digits'),
|
||||
YubiKeyConfigFlag('OATH_FIXED_MODHEX1', 0x10, min_ykver=(2, 1), mode='OATH', doc='First byte in fixed part sent as modhex'),
|
||||
YubiKeyConfigFlag('OATH_FIXED_MODHEX2', 0x40, min_ykver=(2, 1), mode='OATH', doc='First two bytes in fixed part sent as modhex'),
|
||||
YubiKeyConfigFlag('OATH_FIXED_MODHEX', 0x50, min_ykver=(2, 1), mode='OATH', doc='Fixed part sent as modhex'),
|
||||
YubiKeyConfigFlag('OATH_FIXED_MASK', 0x50, min_ykver=(2, 1), mode='OATH', doc='Mask to get out fixed flags'),
|
||||
|
||||
# YubiKey 2.2 and above
|
||||
YubiKeyConfigFlag('CHAL_YUBICO', 0x20, min_ykver=(2, 2), mode='CHAL', doc='Challenge-response enabled - Yubico OTP mode'),
|
||||
YubiKeyConfigFlag('CHAL_HMAC', 0x22, min_ykver=(2, 2), mode='CHAL', doc='Challenge-response enabled - HMAC-SHA1'),
|
||||
YubiKeyConfigFlag('HMAC_LT64', 0x04, min_ykver=(2, 2), mode='CHAL', doc='Set when HMAC message is less than 64 bytes'),
|
||||
YubiKeyConfigFlag('CHAL_BTN_TRIG', 0x08, min_ykver=(2, 2), mode='CHAL', doc='Challenge-respoonse operation requires button press'),
|
||||
]
|
||||
|
||||
ExtendedFlags = [
|
||||
YubiKeyExtendedFlag('SERIAL_BTN_VISIBLE', 0x01, min_ykver=(2, 2), doc='Serial number visible at startup (button press)'),
|
||||
YubiKeyExtendedFlag('SERIAL_USB_VISIBLE', 0x02, min_ykver=(2, 2), doc='Serial number visible in USB iSerial field'),
|
||||
YubiKeyExtendedFlag('SERIAL_API_VISIBLE', 0x04, min_ykver=(2, 2), doc='Serial number visible via API call'),
|
||||
|
||||
# YubiKey 2.3 and above
|
||||
YubiKeyExtendedFlag('USE_NUMERIC_KEYPAD', 0x08, min_ykver=(2, 3), doc='Use numeric keypad for digits'),
|
||||
YubiKeyExtendedFlag('FAST_TRIG', 0x10, min_ykver=(2, 3), doc='Use fast trig if only cfg1 set'),
|
||||
YubiKeyExtendedFlag('ALLOW_UPDATE', 0x20, min_ykver=(2, 3), doc='Allow update of existing configuration (selected flags + access code)'),
|
||||
YubiKeyExtendedFlag('DORMANT', 0x40, min_ykver=(2, 3), doc='Dormant configuration (can be woken up and flag removed = requires update flag)'),
|
||||
]
|
||||
|
||||
|
||||
class YubiKeyConfigError(yubico_exception.YubicoError):
|
||||
"""
|
||||
Exception raised for YubiKey configuration errors.
|
||||
"""
|
||||
|
||||
|
||||
class YubiKeyConfig(object):
|
||||
"""
|
||||
Base class for configuration of all current types of YubiKeys.
|
||||
"""
|
||||
def __init__(self, ykver=None, capabilities=None, update=False, swap=False,
|
||||
zap=False):
|
||||
"""
|
||||
`ykver' is a tuple (major, minor) with the version number of the key
|
||||
you are planning to apply this configuration to. Not mandated, but
|
||||
will get you an exception when trying to set flags for example, rather
|
||||
than the YubiKey just not operating as expected after programming.
|
||||
|
||||
YubiKey >= 2.3 supports updating certain parts of a configuration
|
||||
(for example turning on/off APPEND_CR) without overwriting others
|
||||
(most notably the stored secret). Set `update' to True if this is
|
||||
what you want. The current programming must have flag 'ALLOW_UPDATE'
|
||||
set to allow configuration update instead of requiring complete
|
||||
reprogramming.
|
||||
|
||||
YubiKey >= 2.3 also supports swapping the configurations, making
|
||||
slot 1 be slot 2 and vice versa. Set swap=True for this.
|
||||
|
||||
YubiKeys support deleting a configuration, setting it in an
|
||||
unprogrammed state. Set zap=True for this.
|
||||
"""
|
||||
if capabilities is None:
|
||||
self.capabilities = yubikey_base.YubiKeyCapabilities(default_answer = True)
|
||||
else:
|
||||
self.capabilities = capabilities
|
||||
|
||||
# Minimum version of YubiKey this configuration will require
|
||||
self.yk_req_version = (0, 0)
|
||||
self.ykver = ykver
|
||||
|
||||
self.fixed = b''
|
||||
self.uid = b''
|
||||
self.key = b''
|
||||
self.access_code = b''
|
||||
|
||||
self.ticket_flags = YubiKeyConfigBits(0x0)
|
||||
self.config_flags = YubiKeyConfigBits(0x0)
|
||||
self.extended_flags = YubiKeyConfigBits(0x0)
|
||||
|
||||
self.unlock_code = b''
|
||||
self._mode = ''
|
||||
if update or swap:
|
||||
self._require_version(major=2, minor=3)
|
||||
self._update_config = update
|
||||
self._swap_slots = swap
|
||||
self._zap = zap
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s instance at %s: mode %s, v=%s/%s, lf=%i, lu=%i, lk=%i, lac=%i, tf=%x, cf=%x, ef=%x, lu=%i, up=%s, sw=%s, z=%s>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self._mode,
|
||||
self.yk_req_version, self.ykver,
|
||||
len(self.fixed),
|
||||
len(self.uid),
|
||||
len(self.key),
|
||||
len(self.access_code),
|
||||
self.ticket_flags.to_integer(),
|
||||
self.config_flags.to_integer(),
|
||||
self.extended_flags.to_integer(),
|
||||
len(self.unlock_code),
|
||||
self._update_config,
|
||||
self._swap_slots,
|
||||
self._zap
|
||||
)
|
||||
|
||||
def version_required(self):
|
||||
"""
|
||||
Return the (major, minor) versions of YubiKey required for this configuration.
|
||||
"""
|
||||
return self.yk_req_version
|
||||
|
||||
def fixed_string(self, data=None):
|
||||
"""
|
||||
The fixed string is used to identify a particular Yubikey device.
|
||||
|
||||
The fixed string is referred to as the 'Token Identifier' in OATH-HOTP mode.
|
||||
|
||||
The length of the fixed string can be set between 0 and 16 bytes.
|
||||
|
||||
Tip: This can also be used to extend the length of a static password.
|
||||
"""
|
||||
old = self.fixed
|
||||
if data != None:
|
||||
new = self._decode_input_string(data)
|
||||
if len(new) <= 16:
|
||||
self.fixed = new
|
||||
else:
|
||||
raise yubico_exception.InputError('The "fixed" string must be 0..16 bytes')
|
||||
return old
|
||||
|
||||
def enable_extended_scan_code_mode(self):
|
||||
"""
|
||||
Extended scan code mode means the Yubikey will output the bytes in
|
||||
the 'fixed string' as scan codes, without modhex encoding the data.
|
||||
|
||||
Because of the way this is stored in the config flags, it is not
|
||||
possible to disable this option once it is enabled (of course, you
|
||||
can abort config update or reprogram the YubiKey again).
|
||||
|
||||
Requires YubiKey 2.x.
|
||||
"""
|
||||
if not self.capabilities.have_extended_scan_code_mode():
|
||||
raise
|
||||
self._require_version(major=2)
|
||||
self.config_flag('SHORT_TICKET', True)
|
||||
self.config_flag('STATIC_TICKET', False)
|
||||
|
||||
def enable_shifted_1(self):
|
||||
"""
|
||||
This will cause a shifted character 1 (typically '!') to be sent before
|
||||
anything else. This can be used to make the YubiKey output qualify as a
|
||||
password with 'special characters', if such is required.
|
||||
|
||||
Because of the way this is stored in the config flags, it is not
|
||||
possible to disable this option once it is enabled (of course, you
|
||||
can abort config update or reprogram the YubiKey again).
|
||||
|
||||
Requires YubiKey 2.x.
|
||||
"""
|
||||
self._require_version(major=2)
|
||||
self.config_flag('STRONG_PW2', True)
|
||||
self.config_flag('SEND_REF', True)
|
||||
|
||||
def aes_key(self, data):
|
||||
"""
|
||||
AES128 key to program into YubiKey.
|
||||
|
||||
Supply data as either a raw string, or a hexlified string prefixed by 'h:'.
|
||||
The result, after any hex decoding, must be 16 bytes.
|
||||
"""
|
||||
old = self.key
|
||||
if data:
|
||||
new = self._decode_input_string(data)
|
||||
if len(new) == 16:
|
||||
self.key = new
|
||||
else:
|
||||
raise yubico_exception.InputError('AES128 key must be exactly 16 bytes')
|
||||
|
||||
return old
|
||||
|
||||
def unlock_key(self, data):
|
||||
"""
|
||||
Access code to allow re-programming of your YubiKey.
|
||||
|
||||
Supply data as either a raw bytestring, or a hexlified bytestring prefixed by 'h:'.
|
||||
The result, after any hex decoding, must be 6 bytes.
|
||||
"""
|
||||
if data.startswith(b'h:'):
|
||||
new = binascii.unhexlify(data[2:])
|
||||
else:
|
||||
new = data
|
||||
if len(new) == 6:
|
||||
self.unlock_code = new
|
||||
if not self.access_code:
|
||||
# Don't reset the access code when programming, unless that seems
|
||||
# to be the intent of the calling program.
|
||||
self.access_code = new
|
||||
else:
|
||||
raise yubico_exception.InputError('Unlock key must be exactly 6 bytes')
|
||||
|
||||
def access_key(self, data):
|
||||
"""
|
||||
Set a new access code which will be required for future re-programmings of your YubiKey.
|
||||
|
||||
Supply data as either a raw string, or a hexlified string prefixed by 'h:'.
|
||||
The result, after any hex decoding, must be 6 bytes.
|
||||
"""
|
||||
if data.startswith(b'h:'):
|
||||
new = binascii.unhexlify(data[2:])
|
||||
else:
|
||||
new = data
|
||||
if len(new) == 6:
|
||||
self.access_code = new
|
||||
else:
|
||||
raise yubico_exception.InputError('Access key must be exactly 6 bytes')
|
||||
|
||||
def mode_yubikey_otp(self, private_uid, aes_key):
|
||||
"""
|
||||
Set the YubiKey up for standard OTP validation.
|
||||
"""
|
||||
if not self.capabilities.have_yubico_OTP():
|
||||
raise yubikey_base.YubiKeyVersionError('Yubico OTP not available in %s version %d.%d' \
|
||||
% (self.capabilities.model, self.ykver[0], self.ykver[1]))
|
||||
if private_uid.startswith(b'h:'):
|
||||
private_uid = binascii.unhexlify(private_uid[2:])
|
||||
if len(private_uid) != yubikey_defs.UID_SIZE:
|
||||
raise yubico_exception.InputError('Private UID must be %i bytes' % (yubikey_defs.UID_SIZE))
|
||||
|
||||
self._change_mode('YUBIKEY_OTP', major=0, minor=9)
|
||||
self.uid = private_uid
|
||||
self.aes_key(aes_key)
|
||||
|
||||
def mode_oath_hotp(self, secret, digits=6, factor_seed=None, omp=0x0, tt=0x0, mui=''):
|
||||
"""
|
||||
Set the YubiKey up for OATH-HOTP operation.
|
||||
|
||||
Requires YubiKey 2.1.
|
||||
"""
|
||||
if not self.capabilities.have_OATH('HOTP'):
|
||||
raise yubikey_base.YubiKeyVersionError('OATH HOTP not available in %s version %d.%d' \
|
||||
% (self.capabilities.model, self.ykver[0], self.ykver[1]))
|
||||
if digits != 6 and digits != 8:
|
||||
raise yubico_exception.InputError('OATH-HOTP digits must be 6 or 8')
|
||||
|
||||
self._change_mode('OATH_HOTP', major=2, minor=1)
|
||||
self._set_20_bytes_key(secret)
|
||||
if digits == 8:
|
||||
self.config_flag('OATH_HOTP8', True)
|
||||
if omp or tt or mui:
|
||||
decoded_mui = self._decode_input_string(mui)
|
||||
fixed = yubico_util.chr_byte(omp) + yubico_util.chr_byte(tt) + decoded_mui
|
||||
self.fixed_string(fixed)
|
||||
if factor_seed:
|
||||
self.uid = self.uid + struct.pack('<H', factor_seed)
|
||||
|
||||
def mode_challenge_response(self, secret, type='HMAC', variable=True, require_button=False):
|
||||
"""
|
||||
Set the YubiKey up for challenge-response operation.
|
||||
|
||||
`type' can be 'HMAC' or 'OTP'.
|
||||
|
||||
`variable' is only applicable to type 'HMAC'.
|
||||
|
||||
For type HMAC, `secret' is expected to be 20 bytes (160 bits).
|
||||
For type OTP, `secret' is expected to be 16 bytes (128 bits).
|
||||
|
||||
Requires YubiKey 2.2.
|
||||
"""
|
||||
if not type.upper() in ['HMAC', 'OTP']:
|
||||
raise yubico_exception.InputError('Invalid \'type\' (%s)' % type)
|
||||
if not self.capabilities.have_challenge_response(type.upper()):
|
||||
raise yubikey_base.YubiKeyVersionError('%s Challenge-Response not available in %s version %d.%d' \
|
||||
% (type.upper(), self.capabilities.model, \
|
||||
self.ykver[0], self.ykver[1]))
|
||||
self._change_mode('CHAL_RESP', major=2, minor=2)
|
||||
if type.upper() == 'HMAC':
|
||||
self.config_flag('CHAL_HMAC', True)
|
||||
self.config_flag('HMAC_LT64', variable)
|
||||
self._set_20_bytes_key(secret)
|
||||
else:
|
||||
# type is 'OTP', checked above
|
||||
self.config_flag('CHAL_YUBICO', True)
|
||||
self.aes_key(secret)
|
||||
self.config_flag('CHAL_BTN_TRIG', require_button)
|
||||
|
||||
def ticket_flag(self, which, new=None):
|
||||
"""
|
||||
Get or set a ticket flag.
|
||||
|
||||
'which' can be either a string ('APPEND_CR' etc.), or an integer.
|
||||
You should ALWAYS use a string, unless you really know what you are doing.
|
||||
"""
|
||||
flag = _get_flag(which, TicketFlags)
|
||||
if flag:
|
||||
if not self.capabilities.have_ticket_flag(flag):
|
||||
raise yubikey_base.YubiKeyVersionError('Ticket flag %s requires %s, and this is %s %d.%d'
|
||||
% (which, flag.req_string(self.capabilities.model), \
|
||||
self.capabilities.model, self.ykver[0], self.ykver[1]))
|
||||
req_major, req_minor = flag.req_version()
|
||||
self._require_version(major=req_major, minor=req_minor)
|
||||
value = flag.to_integer()
|
||||
else:
|
||||
if type(which) is not int:
|
||||
raise yubico_exception.InputError('Unknown non-integer TicketFlag (%s)' % which)
|
||||
value = which
|
||||
|
||||
return self.ticket_flags.get_set(value, new)
|
||||
|
||||
def config_flag(self, which, new=None):
|
||||
"""
|
||||
Get or set a config flag.
|
||||
|
||||
'which' can be either a string ('PACING_20MS' etc.), or an integer.
|
||||
You should ALWAYS use a string, unless you really know what you are doing.
|
||||
"""
|
||||
flag = _get_flag(which, ConfigFlags)
|
||||
if flag:
|
||||
if not self.capabilities.have_config_flag(flag):
|
||||
raise yubikey_base.YubiKeyVersionError('Config flag %s requires %s, and this is %s %d.%d'
|
||||
% (which, flag.req_string(self.capabilities.model), \
|
||||
self.capabilities.model, self.ykver[0], self.ykver[1]))
|
||||
req_major, req_minor = flag.req_version()
|
||||
self._require_version(major=req_major, minor=req_minor)
|
||||
value = flag.to_integer()
|
||||
else:
|
||||
if type(which) is not int:
|
||||
raise yubico_exception.InputError('Unknown non-integer ConfigFlag (%s)' % which)
|
||||
value = which
|
||||
|
||||
return self.config_flags.get_set(value, new)
|
||||
|
||||
def extended_flag(self, which, new=None):
|
||||
"""
|
||||
Get or set a extended flag.
|
||||
|
||||
'which' can be either a string ('SERIAL_API_VISIBLE' etc.), or an integer.
|
||||
You should ALWAYS use a string, unless you really know what you are doing.
|
||||
"""
|
||||
flag = _get_flag(which, ExtendedFlags)
|
||||
if flag:
|
||||
if not self.capabilities.have_extended_flag(flag):
|
||||
raise yubikey_base.YubiKeyVersionError('Extended flag %s requires %s, and this is %s %d.%d'
|
||||
% (which, flag.req_string(self.capabilities.model), \
|
||||
self.capabilities.model, self.ykver[0], self.ykver[1]))
|
||||
req_major, req_minor = flag.req_version()
|
||||
self._require_version(major=req_major, minor=req_minor)
|
||||
value = flag.to_integer()
|
||||
else:
|
||||
if type(which) is not int:
|
||||
raise yubico_exception.InputError('Unknown non-integer ExtendedFlag (%s)' % which)
|
||||
value = which
|
||||
|
||||
return self.extended_flags.get_set(value, new)
|
||||
|
||||
def to_string(self):
|
||||
"""
|
||||
Return the current configuration as a bytestring (always 64 bytes).
|
||||
"""
|
||||
#define UID_SIZE 6 /* Size of secret ID field */
|
||||
#define FIXED_SIZE 16 /* Max size of fixed field */
|
||||
#define KEY_SIZE 16 /* Size of AES key */
|
||||
#define KEY_SIZE_OATH 20 /* Size of OATH-HOTP key (key field + first 4 of UID field) */
|
||||
#define ACC_CODE_SIZE 6 /* Size of access code to re-program device */
|
||||
#
|
||||
#struct config_st {
|
||||
# unsigned char fixed[FIXED_SIZE];/* Fixed data in binary format */
|
||||
# unsigned char uid[UID_SIZE]; /* Fixed UID part of ticket */
|
||||
# unsigned char key[KEY_SIZE]; /* AES key */
|
||||
# unsigned char accCode[ACC_CODE_SIZE]; /* Access code to re-program device */
|
||||
# unsigned char fixedSize; /* Number of bytes in fixed field (0 if not used) */
|
||||
# unsigned char extFlags; /* Extended flags */
|
||||
# unsigned char tktFlags; /* Ticket configuration flags */
|
||||
# unsigned char cfgFlags; /* General configuration flags */
|
||||
# unsigned char rfu[2]; /* Reserved for future use */
|
||||
# unsigned short crc; /* CRC16 value of all fields */
|
||||
#};
|
||||
t_rfu = 0
|
||||
|
||||
first = struct.pack('<16s6s16s6sBBBBH',
|
||||
self.fixed,
|
||||
self.uid,
|
||||
self.key,
|
||||
self.access_code,
|
||||
len(self.fixed),
|
||||
self.extended_flags.to_integer(),
|
||||
self.ticket_flags.to_integer(),
|
||||
self.config_flags.to_integer(),
|
||||
t_rfu
|
||||
)
|
||||
|
||||
crc = 0xffff - yubico_util.crc16(first)
|
||||
|
||||
second = first + struct.pack('<H', crc) + self.unlock_code
|
||||
return second
|
||||
|
||||
def to_frame(self, slot=1):
|
||||
"""
|
||||
Return the current configuration as a YubiKeyFrame object.
|
||||
"""
|
||||
data = self.to_string()
|
||||
payload = data.ljust(64, yubico_util.chr_byte(0x0))
|
||||
if slot is 1:
|
||||
if self._update_config:
|
||||
command = SLOT.UPDATE1
|
||||
else:
|
||||
command = SLOT.CONFIG
|
||||
elif slot is 2:
|
||||
if self._update_config:
|
||||
command = SLOT.UPDATE2
|
||||
else:
|
||||
command = SLOT.CONFIG2
|
||||
else:
|
||||
assert()
|
||||
|
||||
if self._swap_slots:
|
||||
command = SLOT.SWAP
|
||||
|
||||
if self._zap:
|
||||
payload = b''
|
||||
|
||||
return yubikey_frame.YubiKeyFrame(command=command, payload=payload)
|
||||
|
||||
def _require_version(self, major, minor=0):
|
||||
""" Update the minimum version of YubiKey this configuration can be applied to. """
|
||||
new_ver = (major, minor)
|
||||
if self.ykver and new_ver > self.ykver:
|
||||
raise yubikey_base.YubiKeyVersionError('Configuration requires YubiKey %d.%d, and this is %d.%d'
|
||||
% (major, minor, self.ykver[0], self.ykver[1]))
|
||||
if new_ver > self.yk_req_version:
|
||||
self.yk_req_version = new_ver
|
||||
|
||||
def _decode_input_string(self, data):
|
||||
if sys.version_info >= (3, 0) and isinstance(data, str):
|
||||
data = data.encode('ascii')
|
||||
if data.startswith(b'm:'):
|
||||
data = b'h:' + yubico_util.modhex_decode(data[2:])
|
||||
if data.startswith(b'h:'):
|
||||
return(binascii.unhexlify(data[2:]))
|
||||
else:
|
||||
return(data)
|
||||
|
||||
def _change_mode(self, mode, major, minor):
|
||||
""" Change mode of operation, with some sanity checks. """
|
||||
if self._mode:
|
||||
if self._mode != mode:
|
||||
raise RuntimeError('Can\'t change mode (from %s to %s)' % (self._mode, mode))
|
||||
self._require_version(major=major, minor=minor)
|
||||
self._mode = mode
|
||||
# when setting mode, we reset all flags
|
||||
self.ticket_flags = YubiKeyConfigBits(0x0)
|
||||
self.config_flags = YubiKeyConfigBits(0x0)
|
||||
self.extended_flags = YubiKeyConfigBits(0x0)
|
||||
if mode != 'YUBIKEY_OTP':
|
||||
self.ticket_flag(mode, True)
|
||||
|
||||
def _set_20_bytes_key(self, data):
|
||||
"""
|
||||
Set a 20 bytes key. This is used in CHAL_HMAC and OATH_HOTP mode.
|
||||
|
||||
Supply data as either a raw bytestring, or a hexlified bytestring prefixed by 'h:'.
|
||||
The result, after any hex decoding, must be 20 bytes.
|
||||
"""
|
||||
if data.startswith(b'h:'):
|
||||
new = binascii.unhexlify(data[2:])
|
||||
else:
|
||||
new = data
|
||||
if len(new) == 20:
|
||||
self.key = new[:16]
|
||||
self.uid = new[16:]
|
||||
else:
|
||||
raise yubico_exception.InputError('HMAC key must be exactly 20 bytes')
|
||||
|
||||
|
||||
def _get_flag(which, flags):
|
||||
""" Find 'which' entry in 'flags'. """
|
||||
res = [this for this in flags if this.is_equal(which)]
|
||||
if len(res) == 0:
|
||||
return None
|
||||
if len(res) == 1:
|
||||
return res[0]
|
||||
assert()
|
||||
171
yubico/yubikey_config_util.py
Normal file
171
yubico/yubikey_config_util.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
utility functions used in yubikey_config.
|
||||
"""
|
||||
# Copyright (c) 2010, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
# classes
|
||||
'YubiKeyConfigBits',
|
||||
'YubiKeyConfigFlag',
|
||||
'YubiKeyExtendedFlag',
|
||||
'YubiKeyTicketFlag',
|
||||
]
|
||||
|
||||
|
||||
class YubiKeyFlag(object):
|
||||
"""
|
||||
A flag value, and associated metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, key, value, doc=None, min_ykver=(0, 0), max_ykver=None, models=['YubiKey', 'YubiKey NEO', 'YubiKey 4']):
|
||||
"""
|
||||
Metadata about a ticket/config/extended flag bit.
|
||||
|
||||
@param key: Name of flag, such as 'APPEND_CR'
|
||||
@param value: Bit value, 0x20 for APPEND_CR
|
||||
@param doc: Human readable description of flag
|
||||
@param min_ykver: Tuple with the minimum version required (major, minor,)
|
||||
@param min_ykver: Tuple with the maximum version required (major, minor,) (for depreacted flags)
|
||||
@param models: List of model identifiers (strings) that support this flag
|
||||
"""
|
||||
if type(key) is not str:
|
||||
assert()
|
||||
if type(value) is not int:
|
||||
assert()
|
||||
if type(min_ykver) is not tuple:
|
||||
assert()
|
||||
if type(models) is not list:
|
||||
assert()
|
||||
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.doc = doc
|
||||
self.min_ykver = min_ykver
|
||||
self.max_ykver = max_ykver
|
||||
self.models = models
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s instance at %s: %s (0x%x)>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.key,
|
||||
self.value
|
||||
)
|
||||
|
||||
def is_equal(self, key):
|
||||
""" Check if key is equal to that of this instance """
|
||||
return self.key == key
|
||||
|
||||
def to_integer(self):
|
||||
""" Return flag value """
|
||||
return self.value
|
||||
|
||||
def req_version(self):
|
||||
""" Return the minimum required version """
|
||||
return self.min_ykver
|
||||
|
||||
def req_string(self, model):
|
||||
""" Return string describing model and version requirement. """
|
||||
if model not in self.models:
|
||||
model = self.models
|
||||
if self.min_ykver and self.max_ykver:
|
||||
return "%s %d.%d..%d.%d" % (model, \
|
||||
self.min_ykver[0], self.min_ykver[1], \
|
||||
self.max_ykver[0], self.max_ykver[1], \
|
||||
)
|
||||
if self.max_ykver:
|
||||
return "%s <= %d.%d" % (model, self.max_ykver[0], self.max_ykver[1])
|
||||
|
||||
return "%s >= %d.%d" % (model, self.min_ykver[0], self.min_ykver[1])
|
||||
|
||||
def is_compatible(self, model, version):
|
||||
""" Check if this flag is compatible with a YubiKey of version 'ver'. """
|
||||
if not model in self.models:
|
||||
return False
|
||||
if self.max_ykver:
|
||||
return (version >= self.min_ykver and
|
||||
version <= self.max_ykver)
|
||||
else:
|
||||
return version >= self.min_ykver
|
||||
|
||||
|
||||
class YubiKeyTicketFlag(YubiKeyFlag):
|
||||
"""
|
||||
A ticket flag value, and associated metadata.
|
||||
"""
|
||||
|
||||
|
||||
class YubiKeyConfigFlag(YubiKeyFlag):
|
||||
"""
|
||||
A config flag value, and associated metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, key, value, mode='', doc=None, min_ykver=(0, 0), max_ykver=None):
|
||||
if type(mode) is not str:
|
||||
assert()
|
||||
self.mode = mode
|
||||
|
||||
super(YubiKeyConfigFlag, self).__init__(key, value, doc=doc, min_ykver=min_ykver, max_ykver=max_ykver)
|
||||
|
||||
|
||||
class YubiKeyExtendedFlag(YubiKeyFlag):
|
||||
"""
|
||||
An extended flag value, and associated metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, key, value, mode='', doc=None, min_ykver=(2, 2), max_ykver=None):
|
||||
if type(mode) is not str:
|
||||
assert()
|
||||
self.mode = mode
|
||||
|
||||
super(YubiKeyExtendedFlag, self).__init__(key, value, doc=doc, min_ykver=min_ykver, max_ykver=max_ykver)
|
||||
|
||||
|
||||
class YubiKeyConfigBits(object):
|
||||
"""
|
||||
Class to hold bit values for configuration.
|
||||
"""
|
||||
def __init__(self, default=0x0):
|
||||
self.value = default
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s instance at %s: value 0x%x>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.value,
|
||||
)
|
||||
|
||||
def get_set(self, flag, new):
|
||||
"""
|
||||
Return the boolean value of 'flag'. If 'new' is set,
|
||||
the flag is updated, and the value before update is
|
||||
returned.
|
||||
"""
|
||||
old = self._is_set(flag)
|
||||
if new is True:
|
||||
self._set(flag)
|
||||
elif new is False:
|
||||
self._clear(flag)
|
||||
return old
|
||||
|
||||
def to_integer(self):
|
||||
""" Return the sum of all flags as an integer. """
|
||||
return self.value
|
||||
|
||||
def _is_set(self, flag):
|
||||
""" Check if flag is set. Returns True or False. """
|
||||
return self.value & flag == flag
|
||||
|
||||
def _set(self, flag):
|
||||
""" Set flag. """
|
||||
self.value |= flag
|
||||
|
||||
def _clear(self, flag):
|
||||
""" Clear flag. """
|
||||
self.value &= (0xff - flag)
|
||||
211
yubico/yubikey_defs.py
Normal file
211
yubico/yubikey_defs.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Module with constants. Many of them from ykdefs.h.
|
||||
"""
|
||||
# Copyright (c) 2010, 2011 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
'RESP_TIMEOUT_WAIT_MASK',
|
||||
'RESP_TIMEOUT_WAIT_FLAG',
|
||||
'RESP_PENDING_FLAG',
|
||||
'SLOT_WRITE_FLAG',
|
||||
'SHA1_MAX_BLOCK_SIZE',
|
||||
'SHA1_DIGEST_SIZE',
|
||||
'OTP_CHALRESP_SIZE',
|
||||
'UID_SIZE',
|
||||
'YUBICO_VID',
|
||||
# functions
|
||||
# classes
|
||||
'SLOT',
|
||||
'MODE',
|
||||
'PID',
|
||||
'YK4_CAPA'
|
||||
]
|
||||
|
||||
from .yubico_version import __version__
|
||||
|
||||
# Yubikey Low level interface #2.3
|
||||
RESP_TIMEOUT_WAIT_MASK = 0x1f # Mask to get timeout value
|
||||
RESP_TIMEOUT_WAIT_FLAG = 0x20 # Waiting for timeout operation - seconds left in lower 5 bits
|
||||
RESP_PENDING_FLAG = 0x40 # Response pending flag
|
||||
SLOT_WRITE_FLAG = 0x80 # Write flag - set by app - cleared by device
|
||||
|
||||
SHA1_MAX_BLOCK_SIZE = 64 # Max size of input SHA1 block
|
||||
SHA1_DIGEST_SIZE = 20 # Size of SHA1 digest = 160 bits
|
||||
OTP_CHALRESP_SIZE = 16 # Number of bytes returned for an Yubico-OTP challenge (not from ykdef.h)
|
||||
|
||||
UID_SIZE = 6 # Size of secret ID field
|
||||
|
||||
|
||||
class SLOT(object):
|
||||
"""Slot entries"""
|
||||
CONFIG = 0x01 # First (default / V1) configuration
|
||||
CONFIG2 = 0x03 # Second (V2) configuration
|
||||
|
||||
UPDATE1 = 0x04 # Update slot 1
|
||||
UPDATE2 = 0x05 # Update slot 2
|
||||
SWAP = 0x06 # Swap slot 1 and 2
|
||||
|
||||
NDEF = 0x08 # Write NDEF record
|
||||
NDEF2 = 0x09 # Write NDEF record for slot 2
|
||||
|
||||
DEVICE_SERIAL = 0x10 # Device serial number
|
||||
DEVICE_CONFIG = 0x11 # Write device configuration record
|
||||
SCAN_MAP = 0x12 # Write scancode map
|
||||
YK4_CAPABILITIES = 0x13 # Read YK4 capabilities list
|
||||
|
||||
CHAL_OTP1 = 0x20 # Write 6 byte challenge to slot 1, get Yubico OTP response
|
||||
CHAL_OTP2 = 0x28 # Write 6 byte challenge to slot 2, get Yubico OTP response
|
||||
|
||||
CHAL_HMAC1 = 0x30 # Write 64 byte challenge to slot 1, get HMAC-SHA1 response
|
||||
CHAL_HMAC2 = 0x38 # Write 64 byte challenge to slot 2, get HMAC-SHA1 response
|
||||
|
||||
|
||||
class MODE(object):
|
||||
"""USB modes"""
|
||||
OTP = 0x00 # OTP only
|
||||
CCID = 0x01 # CCID only, no eject
|
||||
OTP_CCID = 0x02 # OTP + CCID composite
|
||||
U2F = 0x03 # U2F mode
|
||||
OTP_U2F = 0x04 # OTP + U2F composite
|
||||
U2F_CCID = 0x05 # U2F + U2F composite
|
||||
OTP_U2F_CCID = 0x06 # OTP + U2F + CCID composite
|
||||
MASK = 0x07 # Mask for mode bits
|
||||
FLAG_EJECT = 0x80 # CCID device supports eject (CCID) / OTP force eject (CCID composite)
|
||||
|
||||
@classmethod
|
||||
def all(cls, otp=False, ccid=False, u2f=False):
|
||||
"""Returns a set of all USB modes, with optional filtering"""
|
||||
modes = set([
|
||||
cls.OTP,
|
||||
cls.CCID,
|
||||
cls.OTP_CCID,
|
||||
cls.U2F,
|
||||
cls.OTP_U2F,
|
||||
cls.U2F_CCID,
|
||||
cls.OTP_U2F_CCID
|
||||
])
|
||||
|
||||
if otp:
|
||||
modes.difference_update(set([
|
||||
cls.CCID,
|
||||
cls.U2F,
|
||||
cls.U2F_CCID
|
||||
]))
|
||||
|
||||
if ccid:
|
||||
modes.difference_update(set([
|
||||
cls.OTP,
|
||||
cls.U2F,
|
||||
cls.OTP_U2F
|
||||
]))
|
||||
|
||||
if u2f:
|
||||
modes.difference_update(set([
|
||||
cls.OTP,
|
||||
cls.CCID,
|
||||
cls.OTP_CCID
|
||||
]))
|
||||
|
||||
return modes
|
||||
|
||||
|
||||
YUBICO_VID = 0x1050 # Global vendor ID
|
||||
|
||||
|
||||
class PID(object):
|
||||
"""USB Product IDs"""
|
||||
YUBIKEY = 0x0010 # Yubikey (version 1 and 2)
|
||||
|
||||
NEO_OTP = 0x0110 # Yubikey NEO - OTP only
|
||||
NEO_OTP_CCID = 0x0111 # Yubikey NEO - OTP and CCID
|
||||
NEO_CCID = 0x0112 # Yubikey NEO - CCID only
|
||||
NEO_U2F = 0x0113 # Yubikey NEO - U2F only
|
||||
NEO_OTP_U2F = 0x0114 # Yubikey NEO - OTP and U2F
|
||||
NEO_U2F_CCID = 0x0115 # Yubikey NEO - U2F and CCID
|
||||
NEO_OTP_U2F_CCID = 0x0116 # Yubikey NEO - OTP, U2F and CCID
|
||||
|
||||
NEO_SKY = 0x0120 # Security Key by Yubico
|
||||
|
||||
YK4_OTP = 0x0401 # Yubikey 4 - OTP only
|
||||
YK4_U2F = 0x0402 # Yubikey 4 - U2F only
|
||||
YK4_OTP_U2F = 0x0403 # Yubikey 4 - OTP and U2F
|
||||
YK4_CCID = 0x0404 # Yubikey 4 - CCID only
|
||||
YK4_OTP_CCID = 0x0405 # Yubikey 4 - OTP and CCID
|
||||
YK4_U2F_CCID = 0x0406 # Yubikey 4 - U2F and CCID
|
||||
YK4_OTP_U2F_CCID = 0x0407 # Yubikey 4 - OTP, U2F and CCID
|
||||
|
||||
PLUS_U2F_OTP = 0x0410 # Yubikey plus - OTP+U2F
|
||||
|
||||
@classmethod
|
||||
def all(cls, otp=False, ccid=False, u2f=False):
|
||||
"""Returns a set of all PIDs, with optional filtering"""
|
||||
pids = set([
|
||||
cls.YUBIKEY,
|
||||
cls.NEO_OTP,
|
||||
cls.NEO_OTP_CCID,
|
||||
cls.NEO_CCID,
|
||||
cls.NEO_U2F,
|
||||
cls.NEO_OTP_U2F,
|
||||
cls.NEO_U2F_CCID,
|
||||
cls.NEO_OTP_U2F_CCID,
|
||||
cls.NEO_SKY,
|
||||
cls.YK4_OTP,
|
||||
cls.YK4_U2F,
|
||||
cls.YK4_OTP_U2F,
|
||||
cls.YK4_CCID,
|
||||
cls.YK4_OTP_CCID,
|
||||
cls.YK4_U2F_CCID,
|
||||
cls.YK4_OTP_U2F_CCID,
|
||||
cls.PLUS_U2F_OTP
|
||||
])
|
||||
|
||||
if otp:
|
||||
pids.difference_update(set([
|
||||
cls.NEO_CCID,
|
||||
cls.NEO_U2F,
|
||||
cls.NEO_U2F_CCID,
|
||||
cls.NEO_SKY,
|
||||
cls.YK4_U2F,
|
||||
cls.YK4_CCID,
|
||||
cls.YK4_U2F_CCID
|
||||
]))
|
||||
|
||||
if ccid:
|
||||
pids.difference_update(set([
|
||||
cls.YUBIKEY,
|
||||
cls.NEO_OTP,
|
||||
cls.NEO_U2F,
|
||||
cls.NEO_OTP_U2F,
|
||||
cls.NEO_SKY,
|
||||
cls.YK4_OTP,
|
||||
cls.YK4_U2F,
|
||||
cls.YK4_OTP_U2F,
|
||||
cls.PLUS_U2F_OTP
|
||||
]))
|
||||
|
||||
if u2f:
|
||||
pids.difference_update(set([
|
||||
cls.YUBIKEY,
|
||||
cls.NEO_OTP,
|
||||
cls.NEO_OTP_CCID,
|
||||
cls.NEO_CCID,
|
||||
cls.YK4_OTP,
|
||||
cls.YK4_CCID,
|
||||
cls.YK4_OTP_CCID
|
||||
]))
|
||||
|
||||
return pids
|
||||
|
||||
|
||||
class YK4_CAPA(object):
|
||||
"""Capability bits in the YK4_CAPA field"""
|
||||
OTP = 0x01 # OTP functionality
|
||||
U2F = 0x02 # U2F functionality
|
||||
CCID = 0x04 # CCID functionality
|
||||
|
||||
class TAG(object):
|
||||
"""Tags for TLV data read from the YK4_CAPABILITIES slot"""
|
||||
CAPA = 0x01 # capabilities
|
||||
SERIAL = 0x02 # serial number
|
||||
134
yubico/yubikey_frame.py
Normal file
134
yubico/yubikey_frame.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
module for creating frames of data that can be sent to a YubiKey
|
||||
"""
|
||||
# Copyright (c) 2010, Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
# classes
|
||||
'YubiKeyFrame',
|
||||
]
|
||||
|
||||
import struct
|
||||
|
||||
from . import yubico_util
|
||||
from . import yubikey_defs
|
||||
from . import yubico_exception
|
||||
from .yubico_version import __version__
|
||||
|
||||
from .yubikey_defs import SLOT
|
||||
|
||||
class YubiKeyFrame:
|
||||
"""
|
||||
Class containing an YKFRAME (as defined in ykdef.h).
|
||||
|
||||
A frame is basically 64 bytes of data. When this is to be sent
|
||||
to a YubiKey, it is put inside 10 USB HID feature reports. Each
|
||||
feature report is 7 bytes of data plus 1 byte of sequencing and
|
||||
flags.
|
||||
"""
|
||||
|
||||
def __init__(self, command, payload=b''):
|
||||
if not payload:
|
||||
payload = b'\x00' * 64
|
||||
if len(payload) != 64:
|
||||
raise yubico_exception.InputError('payload must be empty or 64 bytes')
|
||||
if not isinstance(payload, bytes):
|
||||
raise yubico_exception.InputError('payload must be a bytestring')
|
||||
self.payload = payload
|
||||
self.command = command
|
||||
self.crc = yubico_util.crc16(payload)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s.%s instance at %s: %s>' % (
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.command
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
"""
|
||||
Return the frame as a 70 byte string.
|
||||
"""
|
||||
# From ykdef.h :
|
||||
#
|
||||
# // Frame structure
|
||||
# #define SLOT_DATA_SIZE 64
|
||||
# typedef struct {
|
||||
# unsigned char payload[SLOT_DATA_SIZE];
|
||||
# unsigned char slot;
|
||||
# unsigned short crc;
|
||||
# unsigned char filler[3];
|
||||
# } YKFRAME;
|
||||
filler = b''
|
||||
return struct.pack('<64sBH3s',
|
||||
self.payload, self.command, self.crc, filler)
|
||||
|
||||
def to_feature_reports(self, debug=False):
|
||||
"""
|
||||
Return the frame as an array of 8-byte parts, ready to be sent to a YubiKey.
|
||||
"""
|
||||
rest = self.to_string()
|
||||
seq = 0
|
||||
out = []
|
||||
# When sending a frame to the YubiKey, we can (should) remove any
|
||||
# 7-byte serie that only consists of '\x00', besides the first
|
||||
# and last serie.
|
||||
while rest:
|
||||
this, rest = rest[:7], rest[7:]
|
||||
if seq > 0 and rest:
|
||||
# never skip first or last serie
|
||||
if this != b'\x00\x00\x00\x00\x00\x00\x00':
|
||||
this += yubico_util.chr_byte(yubikey_defs.SLOT_WRITE_FLAG + seq)
|
||||
out.append(self._debug_string(debug, this))
|
||||
else:
|
||||
this += yubico_util.chr_byte(yubikey_defs.SLOT_WRITE_FLAG + seq)
|
||||
out.append(self._debug_string(debug, this))
|
||||
seq += 1
|
||||
return out
|
||||
|
||||
def _debug_string(self, debug, data):
|
||||
"""
|
||||
Annotate a frames data, if debug is True.
|
||||
"""
|
||||
if not debug:
|
||||
return data
|
||||
if self.command in [
|
||||
SLOT.CONFIG,
|
||||
SLOT.CONFIG2,
|
||||
SLOT.UPDATE1,
|
||||
SLOT.UPDATE2,
|
||||
SLOT.SWAP,
|
||||
]:
|
||||
# annotate according to config_st (see ykdef.h)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x80:
|
||||
return (data, "FFFFFFF") # F = Fixed data (16 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x81:
|
||||
return (data, "FFFFFFF")
|
||||
if yubico_util.ord_byte(data[-1]) == 0x82:
|
||||
return (data, "FFUUUUU") # U = UID (6 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x83:
|
||||
return (data, "UKKKKKK") # K = Key (16 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x84:
|
||||
return (data, "KKKKKKK")
|
||||
if yubico_util.ord_byte(data[-1]) == 0x85:
|
||||
return (data, "KKKAAAA") # A = Access code to set (6 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x86:
|
||||
return (data, "AAlETCr") # l = Length of fixed field (1 byte)
|
||||
# E = extFlags (1 byte)
|
||||
# T = tktFlags (1 byte)
|
||||
# C = cfgFlags (1 byte)
|
||||
# r = RFU (2 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x87:
|
||||
return (data, "rCRaaaa") # CR = CRC16 checksum (2 bytes)
|
||||
# a = Access code to use (6 bytes)
|
||||
if yubico_util.ord_byte(data[-1]) == 0x88:
|
||||
return (data, 'aa')
|
||||
# after payload
|
||||
if yubico_util.ord_byte(data[-1]) == 0x89:
|
||||
return (data, " Scr")
|
||||
|
||||
return (data, '')
|
||||
350
yubico/yubikey_neo_usb_hid.py
Normal file
350
yubico/yubikey_neo_usb_hid.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
module for accessing a USB HID YubiKey NEO
|
||||
"""
|
||||
|
||||
# Copyright (c) 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
'uri_identifiers',
|
||||
# functions
|
||||
# classes
|
||||
'YubiKeyNEO_USBHID',
|
||||
'YubiKeyNEO_USBHIDError'
|
||||
]
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
from .yubico_version import __version__
|
||||
from .yubikey_defs import SLOT, MODE
|
||||
from . import yubikey_usb_hid
|
||||
from . import yubikey_base
|
||||
from . import yubikey_frame
|
||||
from . import yubico_exception
|
||||
from . import yubico_util
|
||||
|
||||
# commands from ykdef.h
|
||||
_ACC_CODE_SIZE = 6 # Size of access code to re-program device
|
||||
_NDEF_DATA_SIZE = 54
|
||||
|
||||
# from nfcdef.h
|
||||
_NDEF_URI_TYPE = ord('U')
|
||||
_NDEF_TEXT_TYPE = ord('T')
|
||||
|
||||
# From nfcforum-ts-rtd-uri-1.0.pdf
|
||||
uri_identifiers = [
|
||||
(0x01, "http://www.",),
|
||||
(0x02, "https://www.",),
|
||||
(0x03, "http://",),
|
||||
(0x04, "https://",),
|
||||
(0x05, "tel:",),
|
||||
(0x06, "mailto:",),
|
||||
(0x07, "ftp://anonymous:anonymous@",),
|
||||
(0x08, "ftp://ftp.",),
|
||||
(0x09, "ftps://",),
|
||||
(0x0a, "sftp://",),
|
||||
(0x0b, "smb://",),
|
||||
(0x0c, "nfs://",),
|
||||
(0x0d, "ftp://",),
|
||||
(0x0e, "dav://",),
|
||||
(0x0f, "news:",),
|
||||
(0x10, "telnet://",),
|
||||
(0x11, "imap:",),
|
||||
(0x12, "rtsp://",),
|
||||
(0x13, "urn:",),
|
||||
(0x14, "pop:",),
|
||||
(0x15, "sip:",),
|
||||
(0x16, "sips:",),
|
||||
(0x17, "tftp:",),
|
||||
(0x18, "btspp://",),
|
||||
(0x19, "btl2cap://",),
|
||||
(0x1a, "btgoep://",),
|
||||
(0x1b, "tcpobex://",),
|
||||
(0x1c, "irdaobex://",),
|
||||
(0x1d, "file://",),
|
||||
(0x1e, "urn:epc:id:",),
|
||||
(0x1f, "urn:epc:tag:",),
|
||||
(0x20, "urn:epc:pat:",),
|
||||
(0x21, "urn:epc:raw:",),
|
||||
(0x22, "urn:epc:",),
|
||||
(0x23, "urn:nfc:",),
|
||||
]
|
||||
|
||||
_NDEF_SLOTS = {
|
||||
1: SLOT.NDEF,
|
||||
2: SLOT.NDEF2
|
||||
}
|
||||
|
||||
|
||||
class YubiKeyNEO_USBHIDError(yubico_exception.YubicoError):
|
||||
""" Exception raised for errors with the NEO USB HID communication. """
|
||||
|
||||
|
||||
class YubiKeyNEO_USBHIDCapabilities(yubikey_usb_hid.YubiKeyUSBHIDCapabilities):
|
||||
"""
|
||||
Capabilities of current YubiKey NEO.
|
||||
"""
|
||||
|
||||
def have_challenge_response(self, mode):
|
||||
return self.version >= (3, 0, 0)
|
||||
|
||||
def have_configuration_slot(self, slot):
|
||||
if self.version < (3, 0, 0):
|
||||
return (slot == 1)
|
||||
return slot in [1, 2]
|
||||
|
||||
def have_nfc_ndef(self, slot=1):
|
||||
if self.version < (3, 0, 0):
|
||||
return slot == 1
|
||||
return slot in [1, 2]
|
||||
|
||||
def have_scanmap(self):
|
||||
return self.version >= (3, 0, 0)
|
||||
|
||||
def have_device_config(self):
|
||||
return self.version >= (3, 0, 0)
|
||||
|
||||
def have_usb_mode(self, mode):
|
||||
if not self.have_device_config():
|
||||
return False
|
||||
mode &= ~MODE.FLAG_EJECT # Mask away eject flag
|
||||
return mode in [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
|
||||
class YubiKeyNEO_USBHID(yubikey_usb_hid.YubiKeyUSBHID):
|
||||
"""
|
||||
Class for accessing a YubiKey NEO over USB HID.
|
||||
|
||||
The NEO is very similar to the original YubiKey (YubiKeyUSBHID)
|
||||
but does add the NDEF "slot".
|
||||
|
||||
The NDEF is the tag the YubiKey emmits over it's NFC interface.
|
||||
"""
|
||||
|
||||
model = 'YubiKey NEO'
|
||||
description = 'YubiKey NEO'
|
||||
_capabilities_cls = YubiKeyNEO_USBHIDCapabilities
|
||||
|
||||
def __init__(self, debug=False, skip=0, hid_device=None):
|
||||
"""
|
||||
Find and connect to a YubiKey NEO (USB HID).
|
||||
|
||||
Attributes :
|
||||
skip -- number of YubiKeys to skip
|
||||
debug -- True or False
|
||||
"""
|
||||
super(YubiKeyNEO_USBHID, self).__init__(debug, skip, hid_device)
|
||||
if self.version_num() >= (2, 1, 4,) and \
|
||||
self.version_num() <= (2, 1, 9,):
|
||||
self.description = 'YubiKey NEO BETA'
|
||||
elif self.version_num() < (3, 0, 0):
|
||||
raise yubikey_base.YubiKeyVersionError("Incorrect version for %s" % self)
|
||||
|
||||
def write_ndef(self, ndef, slot=1):
|
||||
"""
|
||||
Write an NDEF tag configuration to the YubiKey NEO.
|
||||
"""
|
||||
if not self.capabilities.have_nfc_ndef(slot):
|
||||
raise yubikey_base.YubiKeyVersionError("NDEF slot %i unsupported in %s" % (slot, self))
|
||||
|
||||
return self._device._write_config(ndef, _NDEF_SLOTS[slot])
|
||||
|
||||
def init_device_config(self, **kwargs):
|
||||
return YubiKeyNEO_DEVICE_CONFIG(**kwargs)
|
||||
|
||||
def write_device_config(self, device_config):
|
||||
"""
|
||||
Write a DEVICE_CONFIG to the YubiKey NEO.
|
||||
"""
|
||||
if not self.capabilities.have_usb_mode(device_config._mode):
|
||||
raise yubikey_base.YubiKeyVersionError("USB mode: %02x not supported for %s" % (device_config._mode, self))
|
||||
return self._device._write_config(device_config, SLOT.DEVICE_CONFIG)
|
||||
|
||||
def write_scan_map(self, scanmap=None):
|
||||
if not self.capabilities.have_scanmap():
|
||||
raise yubikey_base.YubiKeyVersionError("Scanmap not supported in %s" % self)
|
||||
return self._device._write_config(YubiKeyNEO_SCAN_MAP(scanmap), SLOT.SCAN_MAP)
|
||||
|
||||
|
||||
class YubiKeyNEO_NDEF(object):
|
||||
"""
|
||||
Class allowing programming of a YubiKey NEO NDEF.
|
||||
"""
|
||||
|
||||
ndef_type = _NDEF_URI_TYPE
|
||||
ndef_str = None
|
||||
access_code = yubico_util.chr_byte(0x0) * _ACC_CODE_SIZE
|
||||
# For _NDEF_URI_TYPE
|
||||
ndef_uri_rt = 0x0 # No prepending
|
||||
# For _NDEF_TEXT_TYPE
|
||||
ndef_text_lang = b'en'
|
||||
ndef_text_enc = 'UTF-8'
|
||||
|
||||
def __init__(self, data, access_code = None):
|
||||
self.ndef_str = data
|
||||
if access_code is not None:
|
||||
self.access_code = access_code
|
||||
|
||||
def text(self, encoding = 'UTF-8', language = 'en'):
|
||||
"""
|
||||
Configure parameters for NDEF type TEXT.
|
||||
|
||||
@param encoding: The encoding used. Should be either 'UTF-8' or 'UTF16'.
|
||||
@param language: ISO/IANA language code (see RFC 3066).
|
||||
"""
|
||||
self.ndef_type = _NDEF_TEXT_TYPE
|
||||
self.ndef_text_lang = language
|
||||
self.ndef_text_enc = encoding
|
||||
return self
|
||||
|
||||
def type(self, url = False, text = False, other = None):
|
||||
"""
|
||||
Change the NDEF type.
|
||||
"""
|
||||
if (url, text, other) == (True, False, None):
|
||||
self.ndef_type = _NDEF_URI_TYPE
|
||||
elif (url, text, other) == (False, True, None):
|
||||
self.ndef_type = _NDEF_TEXT_TYPE
|
||||
elif (url, text, type(other)) == (False, False, int):
|
||||
self.ndef_type = other
|
||||
else:
|
||||
raise YubiKeyNEO_USBHIDError("Bad or conflicting NDEF type specified")
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
"""
|
||||
Return the current NDEF as a string (always 64 bytes).
|
||||
"""
|
||||
data = self.ndef_str
|
||||
if self.ndef_type == _NDEF_URI_TYPE:
|
||||
data = self._encode_ndef_uri_type(data)
|
||||
elif self.ndef_type == _NDEF_TEXT_TYPE:
|
||||
data = self._encode_ndef_text_params(data)
|
||||
if len(data) > _NDEF_DATA_SIZE:
|
||||
raise YubiKeyNEO_USBHIDError("NDEF payload too long")
|
||||
# typedef struct {
|
||||
# unsigned char len; // Payload length
|
||||
# unsigned char type; // NDEF type specifier
|
||||
# unsigned char data[NDEF_DATA_SIZE]; // Payload size
|
||||
# unsigned char curAccCode[ACC_CODE_SIZE]; // Access code
|
||||
# } YKNDEF;
|
||||
#
|
||||
fmt = '< B B %ss %ss' % (_NDEF_DATA_SIZE, _ACC_CODE_SIZE)
|
||||
first = struct.pack(fmt,
|
||||
len(data),
|
||||
self.ndef_type,
|
||||
data.ljust(_NDEF_DATA_SIZE, b'\0'),
|
||||
self.access_code,
|
||||
)
|
||||
#crc = 0xffff - yubico_util.crc16(first)
|
||||
#second = first + struct.pack('<H', crc) + self.unlock_code
|
||||
return first
|
||||
|
||||
def to_frame(self, slot=SLOT.NDEF):
|
||||
"""
|
||||
Return the current configuration as a YubiKeyFrame object.
|
||||
"""
|
||||
data = self.to_string()
|
||||
payload = data.ljust(64, b'\0')
|
||||
return yubikey_frame.YubiKeyFrame(command = slot, payload = payload)
|
||||
|
||||
def _encode_ndef_uri_type(self, data):
|
||||
"""
|
||||
Implement NDEF URI Identifier Code.
|
||||
|
||||
This is a small hack to replace some well known prefixes (such as http://)
|
||||
with a one byte code. If the prefix is not known, 0x00 is used.
|
||||
"""
|
||||
t = 0x0
|
||||
for (code, prefix) in uri_identifiers:
|
||||
if data[:len(prefix)].decode('latin-1').lower() == prefix:
|
||||
t = code
|
||||
data = data[len(prefix):]
|
||||
break
|
||||
data = yubico_util.chr_byte(t) + data
|
||||
return data
|
||||
|
||||
def _encode_ndef_text_params(self, data):
|
||||
"""
|
||||
Prepend language and enconding information to data, according to
|
||||
nfcforum-ts-rtd-text-1-0.pdf
|
||||
"""
|
||||
status = len(self.ndef_text_lang)
|
||||
if self.ndef_text_enc == 'UTF16':
|
||||
status = status & 0b10000000
|
||||
return yubico_util.chr_byte(status) + self.ndef_text_lang + data
|
||||
|
||||
|
||||
class YubiKeyNEO_DEVICE_CONFIG(object):
|
||||
"""
|
||||
Class allowing programming of a YubiKey NEO DEVICE_CONFIG.
|
||||
"""
|
||||
|
||||
_mode = MODE.OTP
|
||||
_cr_timeout = 0
|
||||
_auto_eject_time = 0
|
||||
|
||||
|
||||
def __init__(self, mode=MODE.OTP):
|
||||
self._mode = mode
|
||||
|
||||
def cr_timeout(self, timeout = 0):
|
||||
"""
|
||||
Configure the challenge-response timeout in seconds.
|
||||
"""
|
||||
self._cr_timeout = timeout
|
||||
return self
|
||||
|
||||
def auto_eject_time(self, auto_eject_time = 0):
|
||||
"""
|
||||
Configure the auto eject time in 10x seconds.
|
||||
"""
|
||||
self._auto_eject_time = auto_eject_time
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
"""
|
||||
Return the current DEVICE_CONFIG as a string (always 4 bytes).
|
||||
"""
|
||||
fmt = '<BBH'
|
||||
first = struct.pack(
|
||||
fmt,
|
||||
self._mode,
|
||||
self._cr_timeout,
|
||||
self._auto_eject_time
|
||||
)
|
||||
|
||||
#crc = 0xffff - yubico_util.crc16(first)
|
||||
#second = first + struct.pack('<H', crc)
|
||||
return first
|
||||
|
||||
def to_frame(self, slot=SLOT.DEVICE_CONFIG):
|
||||
"""
|
||||
Return the current configuration as a YubiKeyFrame object.
|
||||
"""
|
||||
data = self.to_string()
|
||||
payload = data.ljust(64, b'\0')
|
||||
return yubikey_frame.YubiKeyFrame(command=slot, payload=payload)
|
||||
|
||||
|
||||
class YubiKeyNEO_SCAN_MAP(object):
|
||||
"""
|
||||
Class allowing programming of a YubiKey NEO scan map.
|
||||
"""
|
||||
|
||||
def __init__(self, scanmap=None):
|
||||
if scanmap:
|
||||
if scanmap.startswith(b'h:'):
|
||||
scanmap = binascii.unhexlify(scanmap[2:])
|
||||
if len(scanmap) != 45:
|
||||
raise yubico_exception.InputError('Scan map must be exactly 45 bytes')
|
||||
self.scanmap = scanmap
|
||||
|
||||
def to_frame(self, slot=SLOT.SCAN_MAP):
|
||||
"""
|
||||
Return the current configuration as a YubiKeyFrame object.
|
||||
"""
|
||||
payload = self.scanmap.ljust(64, b'\0')
|
||||
return yubikey_frame.YubiKeyFrame(command=slot, payload=payload)
|
||||
595
yubico/yubikey_usb_hid.py
Normal file
595
yubico/yubikey_usb_hid.py
Normal file
@@ -0,0 +1,595 @@
|
||||
"""
|
||||
module for accessing a USB HID YubiKey
|
||||
"""
|
||||
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
# functions
|
||||
# classes
|
||||
'YubiKeyUSBHID',
|
||||
'YubiKeyUSBHIDError',
|
||||
'YubiKeyUSBHIDStatus',
|
||||
]
|
||||
|
||||
from .yubico_version import __version__
|
||||
|
||||
from . import yubico_util
|
||||
from . import yubico_exception
|
||||
from . import yubikey_frame
|
||||
from . import yubikey_config
|
||||
from . import yubikey_defs
|
||||
from . import yubikey_base
|
||||
from .yubikey_defs import SLOT, YUBICO_VID, PID
|
||||
from .yubikey_base import YubiKey
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
import usb
|
||||
|
||||
# Various USB/HID parameters
|
||||
_USB_TYPE_CLASS = (0x01 << 5)
|
||||
_USB_RECIP_INTERFACE = 0x01
|
||||
_USB_ENDPOINT_IN = 0x80
|
||||
_USB_ENDPOINT_OUT = 0x00
|
||||
|
||||
_HID_GET_REPORT = 0x01
|
||||
_HID_SET_REPORT = 0x09
|
||||
|
||||
_USB_TIMEOUT_MS = 2000
|
||||
|
||||
# from ykcore_backend.h
|
||||
_FEATURE_RPT_SIZE = 8
|
||||
_REPORT_TYPE_FEATURE = 0x03
|
||||
|
||||
# dict used to select command for mode+slot in _challenge_response
|
||||
_CMD_CHALLENGE = {'HMAC': {1: SLOT.CHAL_HMAC1, 2: SLOT.CHAL_HMAC2},
|
||||
'OTP': {1: SLOT.CHAL_OTP1, 2: SLOT.CHAL_OTP2},
|
||||
}
|
||||
|
||||
class YubiKeyUSBHIDError(yubico_exception.YubicoError):
|
||||
""" Exception raised for errors with the USB HID communication. """
|
||||
|
||||
|
||||
class YubiKeyUSBHIDCapabilities(yubikey_base.YubiKeyCapabilities):
|
||||
"""
|
||||
Capture the capabilities of the various versions of YubiKeys.
|
||||
|
||||
Overrides just the functions from YubiKeyCapabilities() that are available
|
||||
in one or more versions, leaving the other ones at False through default_answer.
|
||||
"""
|
||||
def __init__(self, model, version, default_answer):
|
||||
super(YubiKeyUSBHIDCapabilities, self).__init__(
|
||||
model=model,
|
||||
version=version,
|
||||
default_answer=default_answer)
|
||||
|
||||
def have_yubico_OTP(self):
|
||||
""" Yubico OTP support has always been available in the standard YubiKey. """
|
||||
return True
|
||||
|
||||
def have_OATH(self, mode):
|
||||
""" OATH HOTP was introduced in YubiKey 2.2. """
|
||||
if mode not in ['HOTP']:
|
||||
return False
|
||||
return (self.version >= (2, 1, 0,))
|
||||
|
||||
def have_challenge_response(self, mode):
|
||||
""" Challenge-response was introduced in YubiKey 2.2. """
|
||||
if mode not in ['HMAC', 'OTP']:
|
||||
return False
|
||||
return (self.version >= (2, 2, 0,))
|
||||
|
||||
def have_serial_number(self):
|
||||
""" Reading serial number was introduced in YubiKey 2.2, but depends on extflags set too. """
|
||||
return (self.version >= (2, 2, 0,))
|
||||
|
||||
def have_ticket_flag(self, flag):
|
||||
return flag.is_compatible(model = self.model, version = self.version)
|
||||
|
||||
def have_config_flag(self, flag):
|
||||
return flag.is_compatible(model = self.model, version = self.version)
|
||||
|
||||
def have_extended_flag(self, flag):
|
||||
return flag.is_compatible(model = self.model, version = self.version)
|
||||
|
||||
def have_extended_scan_code_mode(self):
|
||||
return (self.version >= (2, 0, 0,))
|
||||
|
||||
def have_shifted_1_mode(self):
|
||||
return (self.version >= (2, 0, 0,))
|
||||
|
||||
def have_configuration_slot(self, slot):
|
||||
return (slot in [1, 2])
|
||||
|
||||
|
||||
class YubiKeyHIDDevice(object):
|
||||
"""
|
||||
High-level wrapper for low-level HID commands for a HID based YubiKey.
|
||||
"""
|
||||
|
||||
def __init__(self, debug=False, skip=0):
|
||||
"""
|
||||
Find and connect to a YubiKey (USB HID).
|
||||
|
||||
Attributes :
|
||||
skip -- number of YubiKeys to skip
|
||||
debug -- True or False
|
||||
"""
|
||||
self.debug = debug
|
||||
self._usb_handle = None
|
||||
if not self._open(skip):
|
||||
raise YubiKeyUSBHIDError('YubiKey USB HID initialization failed')
|
||||
self.status()
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Poll YubiKey for status.
|
||||
"""
|
||||
data = self._read()
|
||||
self._status = YubiKeyUSBHIDStatus(data)
|
||||
return self._status
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self._usb_handle:
|
||||
self._close()
|
||||
except (IOError, AttributeError):
|
||||
pass
|
||||
|
||||
def _write_config(self, cfg, slot):
|
||||
""" Write configuration to YubiKey. """
|
||||
old_pgm_seq = self._status.pgm_seq
|
||||
frame = cfg.to_frame(slot=slot)
|
||||
self._debug("Writing %s frame :\n%s\n" % \
|
||||
(yubikey_config.command2str(frame.command), cfg))
|
||||
self._write(frame)
|
||||
self._waitfor_clear(yubikey_defs.SLOT_WRITE_FLAG)
|
||||
# make sure we have a fresh pgm_seq value
|
||||
self.status()
|
||||
self._debug("Programmed slot %i, sequence %i -> %i\n" % (slot, old_pgm_seq, self._status.pgm_seq))
|
||||
|
||||
cfgs = self._status.valid_configs()
|
||||
if not cfgs and self._status.pgm_seq == 0:
|
||||
return
|
||||
if self._status.pgm_seq == old_pgm_seq + 1:
|
||||
return
|
||||
|
||||
raise YubiKeyUSBHIDError('YubiKey programming failed (seq %i not increased (%i))' % \
|
||||
(old_pgm_seq, self._status.pgm_seq))
|
||||
|
||||
def _read_response(self, may_block=False):
|
||||
""" Wait for a response to become available, and read it. """
|
||||
# wait for response to become available
|
||||
res = self._waitfor_set(yubikey_defs.RESP_PENDING_FLAG, may_block)[:7]
|
||||
# continue reading while response pending is set
|
||||
while True:
|
||||
this = self._read()
|
||||
flags = yubico_util.ord_byte(this[7])
|
||||
if flags & yubikey_defs.RESP_PENDING_FLAG:
|
||||
seq = flags & 0b00011111
|
||||
if res and (seq == 0):
|
||||
break
|
||||
res += this[:7]
|
||||
else:
|
||||
break
|
||||
self._write_reset()
|
||||
return res
|
||||
|
||||
def _read(self):
|
||||
""" Read a USB HID feature report from the YubiKey. """
|
||||
request_type = _USB_TYPE_CLASS | _USB_RECIP_INTERFACE | _USB_ENDPOINT_IN
|
||||
value = _REPORT_TYPE_FEATURE << 8 # apparently required for YubiKey 1.3.2, but not 2.2.x
|
||||
recv = self._usb_handle.controlMsg(request_type,
|
||||
_HID_GET_REPORT,
|
||||
_FEATURE_RPT_SIZE,
|
||||
value = value,
|
||||
timeout = _USB_TIMEOUT_MS)
|
||||
if len(recv) != _FEATURE_RPT_SIZE:
|
||||
self._debug("Failed reading %i bytes (got %i) from USB HID YubiKey.\n"
|
||||
% (_FEATURE_RPT_SIZE, recv))
|
||||
raise YubiKeyUSBHIDError('Failed reading from USB HID YubiKey')
|
||||
data = b''.join(yubico_util.chr_byte(c) for c in recv)
|
||||
self._debug("READ : %s" % (yubico_util.hexdump(data, colorize=True)))
|
||||
return data
|
||||
|
||||
def _write(self, frame):
|
||||
"""
|
||||
Write a YubiKeyFrame to the USB HID.
|
||||
|
||||
Includes polling for YubiKey readiness before each write.
|
||||
"""
|
||||
for data in frame.to_feature_reports(debug=self.debug):
|
||||
debug_str = None
|
||||
if self.debug:
|
||||
(data, debug_str) = data
|
||||
# first, we ensure the YubiKey will accept a write
|
||||
self._waitfor_clear(yubikey_defs.SLOT_WRITE_FLAG)
|
||||
self._raw_write(data, debug_str)
|
||||
return True
|
||||
|
||||
def _write_reset(self):
|
||||
"""
|
||||
Reset read mode by issuing a dummy write.
|
||||
"""
|
||||
data = b'\x00\x00\x00\x00\x00\x00\x00\x8f'
|
||||
self._raw_write(data)
|
||||
self._waitfor_clear(yubikey_defs.SLOT_WRITE_FLAG)
|
||||
return True
|
||||
|
||||
def _raw_write(self, data, debug_str = None):
|
||||
"""
|
||||
Write data to YubiKey.
|
||||
"""
|
||||
if self.debug:
|
||||
if not debug_str:
|
||||
debug_str = ''
|
||||
hexdump = yubico_util.hexdump(data, colorize=True)[:-1] # strip LF
|
||||
self._debug("WRITE : %s %s\n" % (hexdump, debug_str))
|
||||
request_type = _USB_TYPE_CLASS | _USB_RECIP_INTERFACE | _USB_ENDPOINT_OUT
|
||||
value = _REPORT_TYPE_FEATURE << 8 # apparently required for YubiKey 1.3.2, but not 2.2.x
|
||||
sent = self._usb_handle.controlMsg(request_type,
|
||||
_HID_SET_REPORT,
|
||||
data,
|
||||
value = value,
|
||||
timeout = _USB_TIMEOUT_MS)
|
||||
if sent != _FEATURE_RPT_SIZE:
|
||||
self.debug("Failed writing %i bytes (wrote %i) to USB HID YubiKey.\n"
|
||||
% (_FEATURE_RPT_SIZE, sent))
|
||||
raise YubiKeyUSBHIDError('Failed talking to USB HID YubiKey')
|
||||
return sent
|
||||
|
||||
def _waitfor_clear(self, mask, may_block=False):
|
||||
"""
|
||||
Wait for the YubiKey to turn OFF the bits in 'mask' in status responses.
|
||||
|
||||
Returns the 8 bytes last read.
|
||||
"""
|
||||
return self._waitfor('nand', mask, may_block)
|
||||
|
||||
def _waitfor_set(self, mask, may_block=False):
|
||||
"""
|
||||
Wait for the YubiKey to turn ON the bits in 'mask' in status responses.
|
||||
|
||||
Returns the 8 bytes last read.
|
||||
"""
|
||||
return self._waitfor('and', mask, may_block)
|
||||
|
||||
def _waitfor(self, mode, mask, may_block, timeout=2):
|
||||
"""
|
||||
Wait for the YubiKey to either turn ON or OFF certain bits in the status byte.
|
||||
|
||||
mode is either 'and' or 'nand'
|
||||
timeout is a number of seconds (precision about ~0.5 seconds)
|
||||
"""
|
||||
finished = False
|
||||
sleep = 0.01
|
||||
# After six sleeps, we've slept 0.64 seconds.
|
||||
wait_num = (timeout * 2) - 1 + 6
|
||||
resp_timeout = False # YubiKey hasn't indicated RESP_TIMEOUT (yet)
|
||||
while not finished:
|
||||
time.sleep(sleep)
|
||||
this = self._read()
|
||||
flags = yubico_util.ord_byte(this[7])
|
||||
|
||||
if flags & yubikey_defs.RESP_TIMEOUT_WAIT_FLAG:
|
||||
if not resp_timeout:
|
||||
resp_timeout = True
|
||||
seconds_left = flags & yubikey_defs.RESP_TIMEOUT_WAIT_MASK
|
||||
self._debug("Device indicates RESP_TIMEOUT (%i seconds left)\n" \
|
||||
% (seconds_left))
|
||||
if may_block:
|
||||
# calculate new wait_num - never more than 20 seconds
|
||||
seconds_left = min(20, seconds_left)
|
||||
wait_num = (seconds_left * 2) - 1 + 6
|
||||
|
||||
if mode is 'nand':
|
||||
if not flags & mask == mask:
|
||||
finished = True
|
||||
else:
|
||||
self._debug("Status %s (0x%x) has not cleared bits %s (0x%x)\n"
|
||||
% (bin(flags), flags, bin(mask), mask))
|
||||
elif mode is 'and':
|
||||
if flags & mask == mask:
|
||||
finished = True
|
||||
else:
|
||||
self._debug("Status %s (0x%x) has not set bits %s (0x%x)\n"
|
||||
% (bin(flags), flags, bin(mask), mask))
|
||||
else:
|
||||
assert()
|
||||
|
||||
if not finished:
|
||||
wait_num -= 1
|
||||
if wait_num == 0:
|
||||
if mode is 'nand':
|
||||
reason = 'Timed out waiting for YubiKey to clear status 0x%x' % mask
|
||||
else:
|
||||
reason = 'Timed out waiting for YubiKey to set status 0x%x' % mask
|
||||
raise yubikey_base.YubiKeyTimeout(reason)
|
||||
sleep = min(sleep + sleep, 0.5)
|
||||
else:
|
||||
return this
|
||||
|
||||
def _open(self, skip=0):
|
||||
""" Perform HID initialization """
|
||||
usb_device = self._get_usb_device(skip)
|
||||
|
||||
if usb_device:
|
||||
usb_conf = usb_device.configurations[0]
|
||||
self._usb_int = usb_conf.interfaces[0][0]
|
||||
else:
|
||||
raise YubiKeyUSBHIDError('No USB YubiKey found')
|
||||
|
||||
try:
|
||||
self._usb_handle = usb_device.open()
|
||||
self._usb_handle.detachKernelDriver(0)
|
||||
except Exception as error:
|
||||
if 'could not detach kernel driver from interface' in str(error):
|
||||
self._debug('The in-kernel-HID driver has already been detached\n')
|
||||
else:
|
||||
self._debug("detachKernelDriver not supported!\n")
|
||||
|
||||
try:
|
||||
self._usb_handle.setConfiguration(1)
|
||||
except usb.USBError:
|
||||
self._debug("Unable to set configuration, ignoring...\n")
|
||||
self._usb_handle.claimInterface(self._usb_int)
|
||||
return True
|
||||
|
||||
def _close(self):
|
||||
"""
|
||||
Release the USB interface again.
|
||||
"""
|
||||
self._usb_handle.releaseInterface()
|
||||
try:
|
||||
# If we're using PyUSB >= 1.0 we can re-attach the kernel driver here.
|
||||
self._usb_handle.dev.attach_kernel_driver(0)
|
||||
except:
|
||||
pass
|
||||
self._usb_int = None
|
||||
self._usb_handle = None
|
||||
return True
|
||||
|
||||
def _get_usb_device(self, skip=0):
|
||||
"""
|
||||
Get YubiKey USB device.
|
||||
|
||||
Optionally allows you to skip n devices, to support multiple attached YubiKeys.
|
||||
"""
|
||||
try:
|
||||
# PyUSB >= 1.0, this is a workaround for a problem with libusbx
|
||||
# on Windows.
|
||||
import usb.core
|
||||
import usb.legacy
|
||||
devices = [usb.legacy.Device(d) for d in usb.core.find(
|
||||
find_all=True, idVendor=YUBICO_VID)]
|
||||
except ImportError:
|
||||
# Using PyUsb < 1.0.
|
||||
import usb
|
||||
devices = [d for bus in usb.busses() for d in bus.devices]
|
||||
for device in devices:
|
||||
if device.idVendor == YUBICO_VID:
|
||||
if device.idProduct in PID.all(otp=True):
|
||||
if skip == 0:
|
||||
return device
|
||||
skip -= 1
|
||||
return None
|
||||
|
||||
def _debug(self, out, print_prefix=True):
|
||||
""" Print out to stderr, if debugging is enabled. """
|
||||
if self.debug:
|
||||
if print_prefix:
|
||||
pre = self.__class__.__name__
|
||||
if hasattr(self, 'debug_prefix'):
|
||||
pre = getattr(self, 'debug_prefix')
|
||||
sys.stderr.write("%s: " % pre)
|
||||
sys.stderr.write(out)
|
||||
|
||||
|
||||
class YubiKeyUSBHID(YubiKey):
|
||||
"""
|
||||
Class for accessing a YubiKey over USB HID.
|
||||
|
||||
This class is for communicating specifically with standard YubiKeys
|
||||
(USB vendor id = 0x1050, product id = 0x10) using USB HID.
|
||||
|
||||
There is another class for the YubiKey NEO BETA, even though that
|
||||
product also goes by product id 0x10 for the BETA versions. The
|
||||
expectation is that the final YubiKey NEO will have it's own product id.
|
||||
|
||||
Tested with YubiKey versions 1.3 and 2.2.
|
||||
"""
|
||||
|
||||
model = 'YubiKey'
|
||||
description = 'YubiKey (or YubiKey NANO)'
|
||||
_capabilities_cls = YubiKeyUSBHIDCapabilities
|
||||
|
||||
def __init__(self, debug=False, skip=0, hid_device=None):
|
||||
"""
|
||||
Find and connect to a YubiKey (USB HID).
|
||||
|
||||
Attributes :
|
||||
skip -- number of YubiKeys to skip
|
||||
debug -- True or False
|
||||
"""
|
||||
super(YubiKeyUSBHID, self).__init__(debug)
|
||||
if hid_device is None:
|
||||
self._device = YubiKeyHIDDevice(debug, skip)
|
||||
else:
|
||||
self._device = hid_device
|
||||
self.capabilities = \
|
||||
self._capabilities_cls(model=self.model,
|
||||
version=self.version_num(),
|
||||
default_answer=False)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s instance at %s: YubiKey version %s>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.version()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '%s (%s)' % (self.model, self.version())
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Poll YubiKey for status.
|
||||
"""
|
||||
return self._device.status()
|
||||
|
||||
def version_num(self):
|
||||
""" Get the YubiKey version as a tuple (major, minor, build). """
|
||||
return self._device._status.ykver()
|
||||
|
||||
def version(self):
|
||||
""" Get the YubiKey version. """
|
||||
return self._device._status.version()
|
||||
|
||||
def serial(self, may_block=True):
|
||||
""" Get the YubiKey serial number (requires YubiKey 2.2). """
|
||||
if not self.capabilities.have_serial_number():
|
||||
raise yubikey_base.YubiKeyVersionError("Serial number unsupported in YubiKey %s" % self.version() )
|
||||
return self._read_serial(may_block)
|
||||
|
||||
def challenge_response(self, challenge, mode='HMAC', slot=1, variable=True, may_block=True):
|
||||
""" Issue a challenge to the YubiKey and return the response (requires YubiKey 2.2). """
|
||||
if not self.capabilities.have_challenge_response(mode):
|
||||
raise yubikey_base.YubiKeyVersionError("%s challenge-response unsupported in YubiKey %s" % (mode, self.version()) )
|
||||
return self._challenge_response(challenge, mode, slot, variable, may_block)
|
||||
|
||||
def init_config(self, **kw):
|
||||
""" Get a configuration object for this type of YubiKey. """
|
||||
return YubiKeyConfigUSBHID(ykver=self.version_num(), \
|
||||
capabilities = self.capabilities, \
|
||||
**kw)
|
||||
|
||||
def write_config(self, cfg, slot=1):
|
||||
""" Write a configuration to the YubiKey. """
|
||||
cfg_req_ver = cfg.version_required()
|
||||
if cfg_req_ver > self.version_num():
|
||||
raise yubikey_base.YubiKeyVersionError('Configuration requires YubiKey version %i.%i (this is %s)' % \
|
||||
(cfg_req_ver[0], cfg_req_ver[1], self.version()))
|
||||
if not self.capabilities.have_configuration_slot(slot):
|
||||
raise YubiKeyUSBHIDError("Can't write configuration to slot %i" % (slot))
|
||||
return self._device._write_config(cfg, slot)
|
||||
|
||||
def _read_serial(self, may_block):
|
||||
""" Read the serial number from a YubiKey > 2.2. """
|
||||
|
||||
frame = yubikey_frame.YubiKeyFrame(command = SLOT.DEVICE_SERIAL)
|
||||
self._device._write(frame)
|
||||
response = self._device._read_response(may_block=may_block)
|
||||
if not yubico_util.validate_crc16(response[:6]):
|
||||
raise YubiKeyUSBHIDError("Read from device failed CRC check")
|
||||
# the serial number is big-endian, although everything else is little-endian
|
||||
serial = struct.unpack('>lxxx', response)
|
||||
return serial[0]
|
||||
|
||||
def _challenge_response(self, challenge, mode, slot, variable, may_block):
|
||||
""" Do challenge-response with a YubiKey > 2.0. """
|
||||
# Check length and pad challenge if appropriate
|
||||
if mode == 'HMAC':
|
||||
if len(challenge) > yubikey_defs.SHA1_MAX_BLOCK_SIZE:
|
||||
raise yubico_exception.InputError('Mode HMAC challenge too big (%i/%i)' \
|
||||
% (yubikey_defs.SHA1_MAX_BLOCK_SIZE, len(challenge)))
|
||||
if len(challenge) < yubikey_defs.SHA1_MAX_BLOCK_SIZE:
|
||||
pad_with = b'\0'
|
||||
if variable and challenge[-1:] == pad_with:
|
||||
pad_with = b'\xff'
|
||||
challenge = challenge.ljust(yubikey_defs.SHA1_MAX_BLOCK_SIZE, pad_with)
|
||||
response_len = yubikey_defs.SHA1_DIGEST_SIZE
|
||||
elif mode == 'OTP':
|
||||
if len(challenge) != yubikey_defs.UID_SIZE:
|
||||
raise yubico_exception.InputError('Mode OTP challenge must be %i bytes (got %i)' \
|
||||
% (yubikey_defs.UID_SIZE, len(challenge)))
|
||||
challenge = challenge.ljust(yubikey_defs.SHA1_MAX_BLOCK_SIZE, b'\0')
|
||||
response_len = 16
|
||||
else:
|
||||
raise yubico_exception.InputError('Invalid mode supplied (%s, valid values are HMAC and OTP)' \
|
||||
% (mode))
|
||||
|
||||
try:
|
||||
command = _CMD_CHALLENGE[mode][slot]
|
||||
except:
|
||||
raise yubico_exception.InputError('Invalid slot specified (%s)' % (slot))
|
||||
|
||||
frame = yubikey_frame.YubiKeyFrame(command=command, payload=challenge)
|
||||
self._device._write(frame)
|
||||
response = self._device._read_response(may_block=may_block)
|
||||
if not yubico_util.validate_crc16(response[:response_len + 2]):
|
||||
raise YubiKeyUSBHIDError("Read from device failed CRC check")
|
||||
return response[:response_len]
|
||||
|
||||
|
||||
class YubiKeyUSBHIDStatus(object):
|
||||
""" Class to represent the status information we get from the YubiKey. """
|
||||
|
||||
CONFIG1_VALID = 0x01 # Bit in touchLevel indicating that configuration 1 is valid (from firmware 2.1)
|
||||
CONFIG2_VALID = 0x02 # Bit in touchLevel indicating that configuration 2 is valid (from firmware 2.1)
|
||||
|
||||
def __init__(self, data):
|
||||
# From ykdef.h :
|
||||
#
|
||||
# struct status_st {
|
||||
# unsigned char versionMajor; /* Firmware version information */
|
||||
# unsigned char versionMinor;
|
||||
# unsigned char versionBuild;
|
||||
# unsigned char pgmSeq; /* Programming sequence number. 0 if no valid configuration */
|
||||
# unsigned short touchLevel; /* Level from touch detector */
|
||||
# };
|
||||
fmt = '<x BBB B H B'
|
||||
self.version_major, \
|
||||
self.version_minor, \
|
||||
self.version_build, \
|
||||
self.pgm_seq, \
|
||||
self.touch_level, \
|
||||
self.flags = struct.unpack(fmt, data)
|
||||
|
||||
def __repr__(self):
|
||||
valid_str = ''
|
||||
flags_str = ''
|
||||
if self.ykver() >= (2,1,0):
|
||||
valid_str = ", valid=%s" % (self.valid_configs())
|
||||
if self.flags:
|
||||
flags_str = " (flags 0x%x)" % (self.flags)
|
||||
return '<%s instance at %s: YubiKey version %s, pgm_seq=%i, touch_level=%i%s%s>' % (
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.version(),
|
||||
self.pgm_seq,
|
||||
self.touch_level,
|
||||
valid_str,
|
||||
flags_str,
|
||||
)
|
||||
|
||||
|
||||
def ykver(self):
|
||||
""" Returns a tuple with the (major, minor, build) version of the YubiKey firmware. """
|
||||
return (self.version_major, self.version_minor, self.version_build)
|
||||
|
||||
def version(self):
|
||||
""" Return the YubiKey firmware version as a string. """
|
||||
version = "%d.%d.%d" % (self.ykver())
|
||||
return version
|
||||
|
||||
def valid_configs(self):
|
||||
""" Return a list of slots having a valid configurtion. Requires firmware 2.1. """
|
||||
if self.ykver() < (2,1,0):
|
||||
raise YubiKeyUSBHIDError('Valid configs unsupported in firmware %s' % (self.version()))
|
||||
res = []
|
||||
if self.touch_level & self.CONFIG1_VALID == self.CONFIG1_VALID:
|
||||
res.append(1)
|
||||
if self.touch_level & self.CONFIG2_VALID == self.CONFIG2_VALID:
|
||||
res.append(2)
|
||||
return res
|
||||
|
||||
|
||||
class YubiKeyConfigUSBHID(yubikey_config.YubiKeyConfig):
|
||||
"""
|
||||
Configuration class for USB HID YubiKeys.
|
||||
"""
|
||||
def __init__(self, ykver, capabilities = None, **kw):
|
||||
super(YubiKeyConfigUSBHID, self).__init__(ykver=ykver, capabilities=capabilities, **kw)
|
||||
Reference in New Issue
Block a user