Import Upstream version 1.3.3

This commit is contained in:
geos_one
2025-08-13 10:23:20 +02:00
commit 65be73d223
43 changed files with 4458 additions and 0 deletions

44
yubico/__init__.py Normal file
View 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

View 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
View 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
View File

@@ -0,0 +1 @@
__version__ = "1.3.3"

67
yubico/yubikey.py Normal file
View 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
View 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
View 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
View 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()

View 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
View 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
View 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, '')

View 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
View 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)