Import Upstream version 1.3.3
This commit is contained in:
commit
65be73d223
26
COPYING
Normal file
26
COPYING
Normal file
@ -0,0 +1,26 @@
|
||||
Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or
|
||||
without modification, are permitted provided that the following
|
||||
conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
8
MANIFEST.in
Normal file
8
MANIFEST.in
Normal file
@ -0,0 +1,8 @@
|
||||
include release.py
|
||||
include COPYING
|
||||
include NEWS
|
||||
include ChangeLog
|
||||
include examples/*
|
||||
include util/*
|
||||
include doc/*
|
||||
recursive-include test *.py
|
33
NEWS
Normal file
33
NEWS
Normal file
@ -0,0 +1,33 @@
|
||||
* Version 1.3.3 (released 2019-02-28)
|
||||
** Fixes in Python 3 compatibility.
|
||||
|
||||
* Version 1.3.2 (released 2016-02-23)
|
||||
** Various fixes to sequence number checking.
|
||||
** Fix issue with using an access code with the debug flag on.
|
||||
|
||||
* Version 1.3.1 (released 2015-10-01)
|
||||
** Fixup release to correct packages listed in last release.
|
||||
|
||||
* Version 1.3.0 (released 2015-10-01)
|
||||
** Added Python 3 compatibility.
|
||||
** Added the ability to zap a slot.
|
||||
** Added support for YubiKey NEO.
|
||||
** Added support for YubiKey 4.
|
||||
|
||||
* Version 1.2.3 (released 2015-03-23)
|
||||
** Added PIDs for newer devices.
|
||||
** Failure to call setConfiguration is now ignored.
|
||||
|
||||
* Version 1.2.2 (released 2015-02-11)
|
||||
** Fixed bug in yubikey-totp using wrong timestamp.
|
||||
** No longer require nose for setup, only for tests.
|
||||
|
||||
* Version 1.2.1 (released 2013-09-05)
|
||||
** Improved Windows compatibility.
|
||||
** Re-attach kernel driver if using PyUSB >= 1.0
|
||||
|
||||
* Version 1.2.0 (released 2013-08-07)
|
||||
** Moved modules into root instead of Lib/.
|
||||
|
||||
* Version 1.1.1 (released 2013-08-05)
|
||||
** Modified release procedure to simplify uploading to PyPI.
|
18
PKG-INFO
Normal file
18
PKG-INFO
Normal file
@ -0,0 +1,18 @@
|
||||
Metadata-Version: 1.2
|
||||
Name: python-yubico
|
||||
Version: 1.3.3
|
||||
Summary: Python code for talking to Yubico's YubiKeys
|
||||
Home-page: https://github.com/Yubico/python-yubico
|
||||
Author: Dain Nilsson
|
||||
Author-email: dain@yubico.com
|
||||
Maintainer: Yubico Open Source Maintainers
|
||||
Maintainer-email: ossmaint@yubico.com
|
||||
License: BSD 2 clause
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: System Administrators
|
80
README
Normal file
80
README
Normal file
@ -0,0 +1,80 @@
|
||||
== python-yubico
|
||||
Python package for talking to YubiKeys.
|
||||
|
||||
=== Introduction
|
||||
The YubiKey is a hardware token for authentication. The main mode of the
|
||||
YubiKey is entering a one time password (or a strong static password) by acting
|
||||
as a USB HID device, but there are things one can do with bi-directional
|
||||
communication:
|
||||
|
||||
1. Configuration. The yubikey_config class should be a feature-wise complete
|
||||
implementation of everything that can be configured on YubiKeys version 1.3
|
||||
to 3.x (besides deprecated functions in YubiKey 1.x).
|
||||
See `examples/configure_nist_test_key` for an example.
|
||||
|
||||
2. Challenge-response. YubiKey 2.2 and later supports HMAC-SHA1 or Yubico
|
||||
challenge-response operations.
|
||||
See `examples/nist_challenge_response` for an example.
|
||||
|
||||
This library makes it easy to use these two features.
|
||||
|
||||
=== Example
|
||||
Here is a trivial usage example :
|
||||
|
||||
[source, python]
|
||||
----
|
||||
#!/usr/bin/env python
|
||||
""" Get version of connected YubiKey. """
|
||||
|
||||
import sys
|
||||
import yubico
|
||||
|
||||
try:
|
||||
yubikey = yubico.find_yubikey(debug=False)
|
||||
print "Version : %s " % yubikey.version()
|
||||
except yubico.yubico_exception.YubicoError as e:
|
||||
print "ERROR: %s" % e.reason
|
||||
sys.exit(1)
|
||||
----
|
||||
|
||||
=== Installation
|
||||
|
||||
==== Using the Ubuntu/Debian package manager
|
||||
If you use a recent Ubuntu release, you should be able to install python-yubico
|
||||
with these commands :
|
||||
|
||||
$ sudo add-apt-repository ppa:yubico/stable
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install python-yubico
|
||||
|
||||
The Launchpad PPA key generated for our packages is 32CBA1A9.
|
||||
|
||||
==== Using Pip
|
||||
python-yubico is installable via pip:
|
||||
|
||||
$ pip install python-yubico
|
||||
|
||||
Or, directly from the source package in the standard Python way:
|
||||
|
||||
$ cd python-yubico-$ver
|
||||
$ python setup.py install
|
||||
|
||||
This requires the `python-setuptools` package. You will also need
|
||||
http://walac.github.io/pyusb[PyUSB], called python-usb in Debian/Ubuntu.
|
||||
`pyusb` is available on PyPI and may be installed with pip: `pip install --pre
|
||||
pyusb` The --pre command-line option indicates that pre-releases of `pyusb`
|
||||
may also be searched (only pre-releases of `pyusb` are available on PyPI, and
|
||||
pip skips pre-releases by default). Note that while both the 0.4 branch and the
|
||||
1.0 branch are supported, the older 0.4 branch doesn't support re-attaching the
|
||||
kernel device driver on close, which will leave the YubiKey in a state where it
|
||||
is unable to output OTPs until it has been unplugged and plugged back in again.
|
||||
|
||||
==== On Windows
|
||||
If you use Windows, you will require a PyUSB backend. Python-yubico has been
|
||||
tested with http://libusbx.org[libusbx] and confirmed working, without the need
|
||||
for replacing the device driver.
|
||||
|
||||
=== License
|
||||
Copyright (c) Yubico AB.
|
||||
Licensed under the BSD 2-clause license.
|
||||
See the file COPYING for full licence statement.
|
220
doc/ykdef.h
Normal file
220
doc/ykdef.h
Normal file
@ -0,0 +1,220 @@
|
||||
/* -*- mode:C; c-file-style: "bsd" -*- */
|
||||
/*****************************************************************************************
|
||||
** **
|
||||
** Y K D E F - Common Yubikey project header **
|
||||
** **
|
||||
** Date / Rev / Sign / Remark **
|
||||
** 06-06-03 / 0.9.0 / J E / Main **
|
||||
** 06-08-25 / 1.0.0 / J E / Rewritten for final spec **
|
||||
** 08-06-03 / 1.3.0 / J E / Added static OTP feature **
|
||||
** 09-06-02 / 2.0.0 / J E / Added version 2 flags **
|
||||
** 09-09-23 / 2.1.0 / J E / Added version 2.1 flags (OATH-HOTP) **
|
||||
** 10-05-01 / 2.2.0 / J E / Added support for 2.2 ext. + frame **
|
||||
** 11-04-15 / 2.3.0 / J E / Added support for 2.3 extensions **
|
||||
** **
|
||||
*****************************************************************************************/
|
||||
|
||||
#ifndef __YKDEF_H_INCLUDED__
|
||||
#define __YKDEF_H_INCLUDED__
|
||||
|
||||
/* We need the structures defined here to be packed byte-wise */
|
||||
#if defined(_WIN32) || defined(__GNUC__)
|
||||
#pragma pack(push, 1)
|
||||
#endif
|
||||
|
||||
/* USB Identity */
|
||||
|
||||
#define YUBICO_VID 0x1050
|
||||
#define YUBIKEY_PID 0x0010
|
||||
|
||||
/* Slot entries */
|
||||
|
||||
#define SLOT_CONFIG 1 /* First (default / V1) configuration */
|
||||
#define SLOT_NAV 2 /* V1 only */
|
||||
#define SLOT_CONFIG2 3 /* Second (V2) configuration */
|
||||
#define SLOT_UPDATE1 4 /* Update slot 1 */
|
||||
#define SLOT_UPDATE2 5 /* Update slot 2 */
|
||||
#define SLOT_SWAP 6 /* Swap slot 1 and 2 */
|
||||
|
||||
#define SLOT_DEVICE_SERIAL 0x10 /* Device serial number */
|
||||
|
||||
#define SLOT_CHAL_OTP1 0x20 /* Write 6 byte challenge to slot 1, get Yubico OTP response */
|
||||
#define SLOT_CHAL_OTP2 0x28 /* Write 6 byte challenge to slot 2, get Yubico OTP response */
|
||||
|
||||
#define SLOT_CHAL_HMAC1 0x30 /* Write 64 byte challenge to slot 1, get HMAC-SHA1 response */
|
||||
#define SLOT_CHAL_HMAC2 0x38 /* Write 64 byte challenge to slot 2, get HMAC-SHA1 response */
|
||||
|
||||
#define RESP_ITEM_MASK 0x07 /* Mask for slice item # in responses */
|
||||
|
||||
#define RESP_TIMEOUT_WAIT_MASK 0x1f /* Mask to get timeout value */
|
||||
#define RESP_TIMEOUT_WAIT_FLAG 0x20 /* Waiting for timeout operation - seconds left in lower 5 bits */
|
||||
#define RESP_PENDING_FLAG 0x40 /* Response pending flag */
|
||||
#define SLOT_WRITE_FLAG 0x80 /* Write flag - set by app - cleared by device */
|
||||
|
||||
#define DUMMY_REPORT_WRITE 0x8f /* Write a dummy report to force update or abort */
|
||||
|
||||
#define SHA1_MAX_BLOCK_SIZE 64 /* Max size of input SHA1 block */
|
||||
#define SHA1_DIGEST_SIZE 20 /* Size of SHA1 digest = 160 bits */
|
||||
|
||||
#define SERIAL_NUMBER_SIZE 4 /* Size of device serial number */
|
||||
|
||||
/* Frame structure */
|
||||
|
||||
#define SLOT_DATA_SIZE 64
|
||||
|
||||
struct frame_st {
|
||||
unsigned char payload[SLOT_DATA_SIZE]; /* Frame payload */
|
||||
unsigned char slot; /* Slot # field */
|
||||
unsigned short crc; /* CRC field */
|
||||
unsigned char filler[3]; /* Filler */
|
||||
};
|
||||
|
||||
/* Ticket structure */
|
||||
|
||||
#define UID_SIZE 6 /* Size of secret ID field */
|
||||
|
||||
struct ticket_st {
|
||||
unsigned char uid[UID_SIZE]; /* Unique (secret) ID */
|
||||
unsigned short useCtr; /* Use counter (incremented by 1 at first use after power up) + usage flag in msb */
|
||||
unsigned short tstpl; /* Timestamp incremented by approx 8Hz (low part) */
|
||||
unsigned char tstph; /* Timestamp (high part) */
|
||||
unsigned char sessionCtr; /* Number of times used within session. 0 for first use. After it wraps from 0xff to 1 */
|
||||
unsigned short rnd; /* Pseudo-random value */
|
||||
unsigned short crc; /* CRC16 value of all fields */
|
||||
};
|
||||
|
||||
/* Activation modifier of sessionUse field (bitfields not uses as they are not portable) */
|
||||
|
||||
#define TICKET_ACT_HIDRPT 0x8000 /* Ticket generated at activation by keyboard (scroll/num/caps) */
|
||||
#define TICKET_CTR_MASK 0x7fff /* Mask for useCtr value (except HID flag) */
|
||||
|
||||
/* Configuration structure */
|
||||
|
||||
#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 - YubiKey 2.? and above */
|
||||
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 */
|
||||
};
|
||||
|
||||
/* Ticket flags **************************************************************/
|
||||
|
||||
/* Yubikey 1 and above */
|
||||
#define TKTFLAG_TAB_FIRST 0x01 /* Send TAB before first part */
|
||||
#define TKTFLAG_APPEND_TAB1 0x02 /* Send TAB after first part */
|
||||
#define TKTFLAG_APPEND_TAB2 0x04 /* Send TAB after second part */
|
||||
#define TKTFLAG_APPEND_DELAY1 0x08 /* Add 0.5s delay after first part */
|
||||
#define TKTFLAG_APPEND_DELAY2 0x10 /* Add 0.5s delay after second part */
|
||||
#define TKTFLAG_APPEND_CR 0x20 /* Append CR as final character */
|
||||
|
||||
/* Yubikey 2 and above */
|
||||
#define TKTFLAG_PROTECT_CFG2 0x80 /* Block update of config 2 unless config 2 is configured and has this bit set */
|
||||
|
||||
/* Configuration flags *******************************************************/
|
||||
|
||||
/* Yubikey 1 and above */
|
||||
#define CFGFLAG_SEND_REF 0x01 /* Send reference string (0..F) before data */
|
||||
#define CFGFLAG_PACING_10MS 0x04 /* Add 10ms intra-key pacing */
|
||||
#define CFGFLAG_PACING_20MS 0x08 /* Add 20ms intra-key pacing */
|
||||
#define CFGFLAG_STATIC_TICKET 0x20 /* Static ticket generation */
|
||||
|
||||
/* Yubikey 1 only */
|
||||
#define CFGFLAG_TICKET_FIRST 0x02 /* Send ticket first (default is fixed part) */
|
||||
#define CFGFLAG_ALLOW_HIDTRIG 0x10 /* Allow trigger through HID/keyboard */
|
||||
|
||||
/* Yubikey 2 and above */
|
||||
#define CFGFLAG_SHORT_TICKET 0x02 /* Send truncated ticket (half length) */
|
||||
#define CFGFLAG_STRONG_PW1 0x10 /* Strong password policy flag #1 (mixed case) */
|
||||
#define CFGFLAG_STRONG_PW2 0x40 /* Strong password policy flag #2 (subtitute 0..7 to digits) */
|
||||
#define CFGFLAG_MAN_UPDATE 0x80 /* Allow manual (local) update of static OTP */
|
||||
|
||||
/* Yubikey 2.1 and above */
|
||||
#define TKTFLAG_OATH_HOTP 0x40 /* OATH HOTP mode */
|
||||
#define CFGFLAG_OATH_HOTP8 0x02 /* Generate 8 digits HOTP rather than 6 digits */
|
||||
#define CFGFLAG_OATH_FIXED_MODHEX1 0x10 /* First byte in fixed part sent as modhex */
|
||||
#define CFGFLAG_OATH_FIXED_MODHEX2 0x40 /* First two bytes in fixed part sent as modhex */
|
||||
#define CFGFLAG_OATH_FIXED_MODHEX 0x50 /* Fixed part sent as modhex */
|
||||
#define CFGFLAG_OATH_FIXED_MASK 0x50 /* Mask to get out fixed flags */
|
||||
|
||||
/* Yubikey 2.2 and above */
|
||||
|
||||
#define TKTFLAG_CHAL_RESP 0x40 /* Challenge-response enabled (both must be set) */
|
||||
#define CFGFLAG_CHAL_YUBICO 0x20 /* Challenge-response enabled - Yubico OTP mode */
|
||||
#define CFGFLAG_CHAL_HMAC 0x22 /* Challenge-response enabled - HMAC-SHA1 */
|
||||
#define CFGFLAG_HMAC_LT64 0x04 /* Set when HMAC message is less than 64 bytes */
|
||||
#define CFGFLAG_CHAL_BTN_TRIG 0x08 /* Challenge-response operation requires button press */
|
||||
|
||||
#define EXTFLAG_SERIAL_BTN_VISIBLE 0x01 /* Serial number visible at startup (button press) */
|
||||
#define EXTFLAG_SERIAL_USB_VISIBLE 0x02 /* Serial number visible in USB iSerial field */
|
||||
#define EXTFLAG_SERIAL_API_VISIBLE 0x04 /* Serial number visible via API call */
|
||||
|
||||
/* V2.3 flags only */
|
||||
|
||||
#define EXTFLAG_USE_NUMERIC_KEYPAD 0x08 /* Use numeric keypad for digits */
|
||||
#define EXTFLAG_FAST_TRIG 0x10 /* Use fast trig if only cfg1 set */
|
||||
#define EXTFLAG_ALLOW_UPDATE 0x20 /* Allow update of existing configuration (selected flags + access code) */
|
||||
#define EXTFLAG_DORMANT 0x40 /* Dormant configuration (can be woken up and flag removed = requires update flag) */
|
||||
|
||||
/* Flags valid for update */
|
||||
|
||||
#define TKTFLAG_UPDATE_MASK (TKTFLAG_TAB_FIRST | TKTFLAG_APPEND_TAB1 | TKTFLAG_APPEND_TAB2 | TKTFLAG_APPEND_DELAY1 | TKTFLAG_APPEND_DELAY2 | TKTFLAG_APPEND_CR)
|
||||
#define CFGFLAG_UPDATE_MASK 0
|
||||
#define EXTFLAG_UPDATE_MASK (EXTFLAG_SERIAL_BTN_VISIBLE | EXTFLAG_SERIAL_USB_VISIBLE | EXTFLAG_SERIAL_API_VISIBLE | EXTFLAG_USE_NUMERIC_KEYPAD | EXTFLAG_FAST_TRIG | EXTFLAG_ALLOW_UPDATE)
|
||||
|
||||
/* Navigation */
|
||||
|
||||
/* NOTE: Navigation isn't available since Yubikey 1.3.5 and is strongly
|
||||
discouraged. */
|
||||
#define MAX_URL 48
|
||||
|
||||
struct nav_st {
|
||||
unsigned char scancode[MAX_URL];/* Scancode (lower 7 bits) */
|
||||
unsigned char scanmod[MAX_URL >> 2]; /* Modifier fields (packed 2 bits each) */
|
||||
unsigned char flags; /* NAVFLAG_xxx flags */
|
||||
unsigned char filler; /* Filler byte */
|
||||
unsigned short crc; /* CRC16 value of all fields */
|
||||
};
|
||||
|
||||
#define SCANMOD_SHIFT 0x80 /* Highest bit in scancode */
|
||||
#define SCANMOD_ALT_GR 0x01 /* Lowest bit in mod */
|
||||
#define SCANMOD_WIN 0x02 /* WIN key */
|
||||
|
||||
/* Navigation flags */
|
||||
|
||||
#define NAVFLAG_INSERT_TRIG 0x01 /* Automatic trigger when device is inserted */
|
||||
#define NAVFLAG_APPEND_TKT 0x02 /* Append ticket to URL */
|
||||
#define NAVFLAG_DUAL_KEY_USAGE 0x04 /* Dual usage of key: Short = ticket Long = Navigate */
|
||||
|
||||
/* Status block */
|
||||
|
||||
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 */
|
||||
};
|
||||
|
||||
#define CONFIG1_VALID 0x01 /* Bit in touchLevel indicating that configuration 1 is valid (from firmware 2.1) */
|
||||
#define CONFIG2_VALID 0x02 /* Bit in touchLevel indicating that configuration 2 is valid (from firmware 2.1) */
|
||||
|
||||
/* Modified hex string mapping */
|
||||
|
||||
#define MODHEX_MAP "cbdefghijklnrtuv"
|
||||
|
||||
#if defined(_WIN32) || defined(__GNUC__)
|
||||
#pragma pack(pop)
|
||||
#endif
|
||||
|
||||
#endif /* __YKDEF_H_INCLUDED__ */
|
37
examples/configure_neo_ndef
Executable file
37
examples/configure_neo_ndef
Executable file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Set up a YubiKey NEO NDEF
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
|
||||
import six
|
||||
|
||||
import yubico
|
||||
import yubico.yubikey_neo_usb_hid
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
sys.stderr.write("Syntax: %s URL\n" % (sys.argv[0]))
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
url = url.encode('utf-8')
|
||||
|
||||
try:
|
||||
YK = yubico.yubikey_neo_usb_hid.YubiKeyNEO_USBHID(debug=True)
|
||||
print("Version : %s " % YK.version())
|
||||
|
||||
ndef = yubico.yubikey_neo_usb_hid.YubiKeyNEO_NDEF(data = url)
|
||||
|
||||
user_input = six.moves.input('Write configuration to YubiKey? [y/N] : ')
|
||||
if user_input in ('y', 'ye', 'yes'):
|
||||
YK.write_ndef(ndef)
|
||||
print("\nSuccess!")
|
||||
else:
|
||||
print("\nAborted")
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print("ERROR: %s" % inst.reason)
|
||||
sys.exit(1)
|
31
examples/configure_nist_test_key
Executable file
31
examples/configure_nist_test_key
Executable file
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Set up a YubiKey with a NIST PUB 198 A.2
|
||||
20 bytes test vector in Slot 2 (variable input)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import yubico
|
||||
import six
|
||||
|
||||
slot=2
|
||||
|
||||
try:
|
||||
YK = yubico.find_yubikey(debug=True)
|
||||
print("Version : %s " % YK.version())
|
||||
|
||||
Cfg = YK.init_config()
|
||||
key = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Cfg.mode_challenge_response(key, type='HMAC', variable=True)
|
||||
Cfg.extended_flag('SERIAL_API_VISIBLE', True)
|
||||
|
||||
user_input = six.moves.input('Write configuration to slot %i of YubiKey? [y/N] : ' % slot )
|
||||
if user_input in ('y', 'ye', 'yes'):
|
||||
YK.write_config(Cfg, slot=slot)
|
||||
print("\nSuccess!")
|
||||
else:
|
||||
print("\nAborted")
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print("ERROR: %s" % inst.reason)
|
||||
sys.exit(1)
|
46
examples/nist_challenge_response
Executable file
46
examples/nist_challenge_response
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test challenge-response, assumes a NIST PUB 198 A.2
|
||||
20 bytes test vector in Slot 2 (variable input)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import yubico
|
||||
|
||||
expected = \
|
||||
b'\x09\x22\xd3\x40\x5f\xaa\x3d\x19\x4f\x82' + \
|
||||
b'\xa4\x58\x30\x73\x7d\x5c\xc6\xc7\x5d\x24'
|
||||
|
||||
# turn on YubiKey debug if -v is given as an argument
|
||||
debug = False
|
||||
if len(sys.argv) > 1:
|
||||
debug = (sys.argv[1] == '-v')
|
||||
|
||||
# Look for and initialize the YubiKey
|
||||
try:
|
||||
YK = yubico.find_yubikey(debug=debug)
|
||||
print("Version : %s " % YK.version())
|
||||
print("Serial : %i" % YK.serial())
|
||||
print("")
|
||||
|
||||
# Do challenge-response
|
||||
secret = b'Sample #2'.ljust(64, b'\0')
|
||||
print("Sending challenge : %s\n" % repr(secret))
|
||||
|
||||
response = YK.challenge_response(secret, slot=2)
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print("ERROR: %s" % inst.reason)
|
||||
sys.exit(1)
|
||||
|
||||
print("Response :\n%s\n" % yubico.yubico_util.hexdump(response))
|
||||
|
||||
# Workaround for http://bugs.python.org/issue24596
|
||||
del YK
|
||||
|
||||
# Check if the response matched the expected one
|
||||
if response == expected:
|
||||
print("OK! Response matches the NIST PUB 198 A.2 expected response.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("ERROR! Response does NOT match the NIST PUB 198 A.2 expected response.")
|
||||
sys.exit(1)
|
247
examples/rolling_challenge_response
Executable file
247
examples/rolling_challenge_response
Executable file
@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2011, Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
#
|
||||
"""
|
||||
Demonstrate rolling challenges.
|
||||
|
||||
This is a scheme for generating "one time" HMAC-SHA1 challenges, which
|
||||
works by being able to access the HMAC-SHA1 key on the host computer every
|
||||
time the correct response is provided.
|
||||
|
||||
GPGME would've been used to encrypt the HMAC-SHA1 with the next expected
|
||||
response, but none of the two Python bindings to gpgme I have available
|
||||
at the moment supports symmetric encryption, so for demo purposes AES CBC
|
||||
is used instead.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hmac
|
||||
import argparse
|
||||
import hashlib
|
||||
import binascii
|
||||
|
||||
import yubico
|
||||
import six
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Parse the command line arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description = "Demonstrate rolling challenges",
|
||||
add_help=True
|
||||
)
|
||||
parser.add_argument('-v', '--verbose',
|
||||
dest='verbose',
|
||||
action='store_true', default=False,
|
||||
help='Enable verbose operation.'
|
||||
)
|
||||
parser.add_argument('--debug',
|
||||
dest='debug',
|
||||
action='store_true', default=False,
|
||||
help='Enable debug operation.'
|
||||
)
|
||||
parser.add_argument('--init',
|
||||
dest='init',
|
||||
action='store_true', default=False,
|
||||
help='Initialize demo.'
|
||||
)
|
||||
parser.add_argument('-F', '--filename',
|
||||
dest='filename',
|
||||
required=True,
|
||||
help='State filename.'
|
||||
)
|
||||
parser.add_argument('--challenge-length',
|
||||
dest='challenge_length',
|
||||
type = int, default = 32,
|
||||
help='Length of challenges generated, in bytes.'
|
||||
)
|
||||
parser.add_argument('--slot',
|
||||
dest='slot',
|
||||
type = int, default = 2,
|
||||
help='YubiKey slot to send challenge to.'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def init_demo(args):
|
||||
""" Initializes the demo by asking a few questions and creating a new stat file. """
|
||||
hmac_key = six.moves.input("Enter HMAC-SHA1 key as 40 chars of hex (or press enter for random key) : ")
|
||||
if hmac_key:
|
||||
try:
|
||||
hmac_key = binascii.unhexlify(hmac_key)
|
||||
except:
|
||||
sys.stderr.write("Could not decode HMAC-SHA1 key. Please enter 40 hex-chars.\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
hmac_key = os.urandom(20)
|
||||
if len(hmac_key) != 20:
|
||||
sys.stderr.write("Decoded HMAC-SHA1 key is %i bytes, expected 20.\n" %( len(hmac_key)))
|
||||
sys.exit(1)
|
||||
|
||||
print("To program a YubiKey >= 2.2 for challenge-response with this key, use :")
|
||||
print("")
|
||||
print(" $ ykpersonalize -%i -ochal-resp -ochal-hmac -ohmac-lt64 -a %s" % (args.slot, binascii.hexlify(hmac_key).decode('ascii')))
|
||||
print("")
|
||||
|
||||
passphrase = six.moves.input("Enter the secret passphrase to protect with the rolling challenges : ")
|
||||
|
||||
secret_dict = {"count": 0,
|
||||
"passphrase": passphrase,
|
||||
}
|
||||
roll_next_challenge(args, hmac_key, secret_dict)
|
||||
|
||||
def do_challenge(args):
|
||||
""" Send a challenge to the YubiKey and use the result to decrypt the state file. """
|
||||
outer_j = load_state_file(args)
|
||||
challenge = outer_j["challenge"]
|
||||
print("Challenge : %s" % (challenge))
|
||||
response = get_yubikey_response(args, binascii.unhexlify(outer_j["challenge"]))
|
||||
if args.debug or args.verbose:
|
||||
print("\nGot %i bytes response %s\n" % (len(response), binascii.hexlify(response)))
|
||||
else:
|
||||
print("Response : %s" % binascii.hexlify(response))
|
||||
inner_j = decrypt_with_response(args, outer_j["inner"], response)
|
||||
if args.verbose or args.debug:
|
||||
print("\nDecrypted 'inner' :\n%s\n" % (inner_j))
|
||||
|
||||
secret_dict = {}
|
||||
try:
|
||||
secret_dict = json.loads(inner_j.decode('ascii'))
|
||||
except ValueError:
|
||||
sys.stderr.write("\nCould not parse decoded data as JSON, you probably did not produce the right response.\n")
|
||||
sys.exit(1)
|
||||
|
||||
secret_dict["count"] += 1
|
||||
|
||||
print("\nThe passphrase protected using rolling challenges is :\n")
|
||||
print("\t%s\n\nAccessed %i times.\n" % (secret_dict["passphrase"], secret_dict["count"]))
|
||||
roll_next_challenge(args, binascii.unhexlify(secret_dict["hmac_key"]), secret_dict)
|
||||
|
||||
def get_yubikey_response(args, challenge):
|
||||
"""
|
||||
Do challenge-response with the YubiKey if one is found. Otherwise prompt user to fake a response. """
|
||||
try:
|
||||
YK = yubico.find_yubikey(debug = args.debug)
|
||||
response = YK.challenge_response(challenge.ljust(64, b'\0'), slot = args.slot)
|
||||
return response
|
||||
except yubico.yubico_exception.YubicoError as e:
|
||||
print("YubiKey challenge-response failed (%s)" % e.reason)
|
||||
print("")
|
||||
response = six.moves.input("Assuming you do not have a YubiKey. Enter repsonse manually (hex encoded) : ")
|
||||
return binascii.unhexlify(response)
|
||||
|
||||
def roll_next_challenge(args, hmac_key, inner_dict):
|
||||
"""
|
||||
When we have the HMAC-SHA1 key in clear, generate a random challenge and compute the
|
||||
expected response for that challenge.
|
||||
|
||||
hmac_key is a 20-byte bytestring
|
||||
"""
|
||||
if len(hmac_key) != 20 or not isinstance(hmac_key, bytes):
|
||||
hmac_key = binascii.unhexlify(hmac_key)
|
||||
|
||||
challenge = os.urandom(args.challenge_length)
|
||||
response = get_response(hmac_key, challenge)
|
||||
|
||||
print("Generated challenge : %s" % binascii.hexlify(challenge).decode('ascii'))
|
||||
print("Expected response : %s (sssh, don't tell anyone)" % binascii.hexlify(response).decode('ascii'))
|
||||
print("")
|
||||
if args.debug or args.verbose or args.init:
|
||||
print("To manually verify that your YubiKey produces this response, use :")
|
||||
print("")
|
||||
print(" $ ykchalresp -%i -x %s" % (args.slot, binascii.hexlify(challenge).decode('ascii')))
|
||||
print("")
|
||||
|
||||
inner_dict["hmac_key"] = binascii.hexlify(hmac_key).decode('ascii')
|
||||
inner_j = json.dumps(inner_dict, indent = 4)
|
||||
if args.verbose or args.debug:
|
||||
print("Inner JSON :\n%s\n" % (inner_j))
|
||||
inner_ciphertext = encrypt_with_response(args, inner_j, response)
|
||||
outer_dict = {"challenge": binascii.hexlify(challenge).decode('ascii'),
|
||||
"inner": inner_ciphertext.decode('ascii'),
|
||||
}
|
||||
outer_j = json.dumps(outer_dict, indent = 4)
|
||||
if args.verbose or args.debug:
|
||||
print("\nOuter JSON :\n%s\n" % (outer_j))
|
||||
|
||||
print("Saving 'outer' JSON to file '%s'" % (args.filename))
|
||||
write_state_file(args, outer_j)
|
||||
|
||||
def get_response(hmac_key, challenge):
|
||||
""" Compute the expected response for `challenge', as hexadecimal string """
|
||||
print(binascii.hexlify(hmac_key), binascii.hexlify(challenge), hashlib.sha1)
|
||||
h = hmac.new(hmac_key, challenge, hashlib.sha1)
|
||||
return h.digest()
|
||||
|
||||
def encrypt_with_response(args, data, key):
|
||||
"""
|
||||
Encrypt our secret inner data with the response we expect the next time.
|
||||
|
||||
NOTE: The use of AES CBC has not been validated as cryptographically sound
|
||||
in this application.
|
||||
|
||||
I would have done this with GPGme if it weren't for the fact that neither
|
||||
of the two versions for Python available in Ubuntu 10.10 have support for
|
||||
symmetric encrypt/decrypt (LP: #295918).
|
||||
"""
|
||||
# pad data to multiple of 16 bytes for AES CBC
|
||||
pad = len(data) % 16
|
||||
data += ' ' * (16 - pad)
|
||||
|
||||
# need to pad key as well
|
||||
aes_key = key
|
||||
aes_key += b'\0' * (32 - len(aes_key))
|
||||
if args.debug:
|
||||
print(("AES-CBC encrypting 'inner' with key (%i bytes) : %s" % (len(aes_key), binascii.hexlify(aes_key))))
|
||||
|
||||
obj = AES.new(aes_key, AES.MODE_CBC, b'\0' * 16)
|
||||
ciphertext = obj.encrypt(data)
|
||||
return binascii.hexlify(ciphertext)
|
||||
|
||||
def decrypt_with_response(args, data, key):
|
||||
"""
|
||||
Try to decrypt the secret inner data with the response we got to this challenge.
|
||||
"""
|
||||
aes_key = key
|
||||
try:
|
||||
aes_key = binascii.unhexlify(key)
|
||||
except (TypeError, binascii.Error):
|
||||
# was not hex encoded
|
||||
pass
|
||||
# need to pad key
|
||||
aes_key += b'\0' * (32 - len(aes_key))
|
||||
if args.debug:
|
||||
print(("AES-CBC decrypting 'inner' using key (%i bytes) : %s" % (len(aes_key), binascii.hexlify(aes_key))))
|
||||
|
||||
obj = AES.new(aes_key, AES.MODE_CBC, b'\0' * 16)
|
||||
plaintext = obj.decrypt(binascii.unhexlify(data))
|
||||
return plaintext
|
||||
|
||||
def write_state_file(args, data):
|
||||
""" Save state to file. """
|
||||
f = open(args.filename, 'w')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
def load_state_file(args):
|
||||
""" Load (and parse) the state file. """
|
||||
return json.loads(open(args.filename).read())
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.init:
|
||||
init_demo(args)
|
||||
else:
|
||||
do_challenge(args)
|
||||
|
||||
print("\nDone\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
46
examples/update_cfg_remove_cr
Executable file
46
examples/update_cfg_remove_cr
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Set up a YubiKey for standard OTP with CR, then remove it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import yubico
|
||||
import six
|
||||
import binascii
|
||||
|
||||
slot=2
|
||||
|
||||
try:
|
||||
YK = yubico.find_yubikey(debug=True)
|
||||
print("Version : %s " % YK.version())
|
||||
print("Status : %s " % YK.status())
|
||||
|
||||
Cfg = YK.init_config()
|
||||
Cfg.extended_flag('ALLOW_UPDATE', True)
|
||||
Cfg.ticket_flag('APPEND_CR', True)
|
||||
Cfg.extended_flag('SERIAL_API_VISIBLE', True)
|
||||
Cfg.uid = binascii.unhexlify('010203040506')
|
||||
Cfg.fixed_string("m:ftccftbbftdd")
|
||||
Cfg.aes_key('h:' + 32 * 'a')
|
||||
|
||||
user_input = six.moves.input('Write configuration to slot %i of YubiKey? [y/N] : ' % slot )
|
||||
if user_input in ('y', 'ye', 'yes'):
|
||||
YK.write_config(Cfg, slot=slot)
|
||||
print("\nSuccess!")
|
||||
print("Status : %s " % YK.status())
|
||||
else:
|
||||
print("\nAborted")
|
||||
sys.exit(0)
|
||||
|
||||
six.moves.input("Press enter to update...")
|
||||
|
||||
Cfg = YK.init_config(update=True)
|
||||
Cfg.ticket_flag('APPEND_CR', False)
|
||||
|
||||
print ("Updating...");
|
||||
YK.write_config(Cfg, slot=slot)
|
||||
print("\nSuccess!")
|
||||
except yubico.yubico_exception.YubicoError as inst:
|
||||
print("ERROR: %s" % inst.reason)
|
||||
sys.exit(1)
|
37
examples/yubikey-inventory
Executable file
37
examples/yubikey-inventory
Executable file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Example of how to access more than one connected YubiKey.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import yubico
|
||||
|
||||
def get_all_yubikeys(debug):
|
||||
"""
|
||||
Look for YubiKey with ever increasing `skip' value until an error is returned.
|
||||
|
||||
Return all instances of class YubiKey we got before failing.
|
||||
"""
|
||||
res = []
|
||||
try:
|
||||
skip = 0
|
||||
while skip < 255:
|
||||
YK = yubico.find_yubikey(debug = debug, skip = skip)
|
||||
res.append(YK)
|
||||
skip += 1
|
||||
except yubico.yubikey.YubiKeyError:
|
||||
pass
|
||||
return res
|
||||
|
||||
debug = False
|
||||
if len(sys.argv) > 1:
|
||||
debug = (sys.argv[1] == '-v')
|
||||
keys = get_all_yubikeys(debug)
|
||||
|
||||
if not keys:
|
||||
print("No YubiKey found.")
|
||||
else:
|
||||
n = 1
|
||||
for this in keys:
|
||||
print("YubiKey #%02i : %s %s" % (n, this.description, this.status()))
|
||||
n += 1
|
18
python_yubico.egg-info/PKG-INFO
Normal file
18
python_yubico.egg-info/PKG-INFO
Normal file
@ -0,0 +1,18 @@
|
||||
Metadata-Version: 1.2
|
||||
Name: python-yubico
|
||||
Version: 1.3.3
|
||||
Summary: Python code for talking to Yubico's YubiKeys
|
||||
Home-page: https://github.com/Yubico/python-yubico
|
||||
Author: Dain Nilsson
|
||||
Author-email: dain@yubico.com
|
||||
Maintainer: Yubico Open Source Maintainers
|
||||
Maintainer-email: ossmaint@yubico.com
|
||||
License: BSD 2 clause
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: System Administrators
|
42
python_yubico.egg-info/SOURCES.txt
Normal file
42
python_yubico.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,42 @@
|
||||
COPYING
|
||||
ChangeLog
|
||||
MANIFEST.in
|
||||
NEWS
|
||||
README
|
||||
release.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
doc/ykdef.h
|
||||
examples/configure_neo_ndef
|
||||
examples/configure_nist_test_key
|
||||
examples/nist_challenge_response
|
||||
examples/rolling_challenge_response
|
||||
examples/update_cfg_remove_cr
|
||||
examples/yubikey-inventory
|
||||
python_yubico.egg-info/PKG-INFO
|
||||
python_yubico.egg-info/SOURCES.txt
|
||||
python_yubico.egg-info/dependency_links.txt
|
||||
python_yubico.egg-info/requires.txt
|
||||
python_yubico.egg-info/top_level.txt
|
||||
test/__init__.py
|
||||
test/soft/__init__.py
|
||||
test/soft/test_yubico.py
|
||||
test/soft/test_yubikey_config.py
|
||||
test/soft/test_yubikey_frame.py
|
||||
test/usb/__init__.py
|
||||
test/usb/test_yubikey_usb_hid.py
|
||||
util/yubikey-totp
|
||||
util/yubikey-totp.1
|
||||
yubico/__init__.py
|
||||
yubico/yubico_exception.py
|
||||
yubico/yubico_util.py
|
||||
yubico/yubico_version.py
|
||||
yubico/yubikey.py
|
||||
yubico/yubikey_4_usb_hid.py
|
||||
yubico/yubikey_base.py
|
||||
yubico/yubikey_config.py
|
||||
yubico/yubikey_config_util.py
|
||||
yubico/yubikey_defs.py
|
||||
yubico/yubikey_frame.py
|
||||
yubico/yubikey_neo_usb_hid.py
|
||||
yubico/yubikey_usb_hid.py
|
1
python_yubico.egg-info/dependency_links.txt
Normal file
1
python_yubico.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
||||
|
1
python_yubico.egg-info/requires.txt
Normal file
1
python_yubico.egg-info/requires.txt
Normal file
@ -0,0 +1 @@
|
||||
pyusb
|
1
python_yubico.egg-info/top_level.txt
Normal file
1
python_yubico.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
yubico
|
149
release.py
Normal file
149
release.py
Normal file
@ -0,0 +1,149 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from distutils import log
|
||||
from distutils.core import Command
|
||||
from distutils.errors import DistutilsSetupError
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
|
||||
class release(Command):
|
||||
description = "create and release a new version"
|
||||
user_options = [
|
||||
('keyid', None, "GPG key to sign with"),
|
||||
('skip-tests', None, "skip running the tests"),
|
||||
('pypi', None, "publish to pypi"),
|
||||
]
|
||||
boolean_options = ['skip-tests', 'pypi']
|
||||
|
||||
def initialize_options(self):
|
||||
self.keyid = None
|
||||
self.skip_tests = 0
|
||||
self.pypi = 0
|
||||
|
||||
def finalize_options(self):
|
||||
self.cwd = os.getcwd()
|
||||
self.fullname = self.distribution.get_fullname()
|
||||
self.name = self.distribution.get_name()
|
||||
self.version = self.distribution.get_version()
|
||||
|
||||
def _verify_version(self):
|
||||
with open('NEWS', 'r') as news_file:
|
||||
line = news_file.readline()
|
||||
now = date.today().strftime('%Y-%m-%d')
|
||||
if not re.search(r'Version %s \(released %s\)' % (self.version, now),
|
||||
line):
|
||||
raise DistutilsSetupError("Incorrect date/version in NEWS!")
|
||||
|
||||
def _verify_tag(self):
|
||||
if os.system('git tag | grep -q "^%s\$"' % self.fullname) == 0:
|
||||
raise DistutilsSetupError(
|
||||
"Tag '%s' already exists!" % self.fullname)
|
||||
|
||||
def _sign(self):
|
||||
if os.path.isfile('dist/%s.tar.gz.asc' % self.fullname):
|
||||
# Signature exists from upload, re-use it:
|
||||
sign_opts = ['--output dist/%s.tar.gz.sig' % self.fullname,
|
||||
'--dearmor dist/%s.tar.gz.asc' % self.fullname]
|
||||
else:
|
||||
# No signature, create it:
|
||||
sign_opts = ['--detach-sign', 'dist/%s.tar.gz' % self.fullname]
|
||||
if self.keyid:
|
||||
sign_opts.insert(1, '--default-key ' + self.keyid)
|
||||
self.execute(os.system, ('gpg ' + (' '.join(sign_opts)),))
|
||||
|
||||
if os.system('gpg --verify dist/%s.tar.gz.sig' % self.fullname) != 0:
|
||||
raise DistutilsSetupError("Error verifying signature!")
|
||||
|
||||
def _tag(self):
|
||||
tag_opts = ['-s', '-m ' + self.fullname, self.fullname]
|
||||
if self.keyid:
|
||||
tag_opts[0] = '-u ' + self.keyid
|
||||
self.execute(os.system, ('git tag ' + (' '.join(tag_opts)),))
|
||||
|
||||
def _do_call_publish(self, cmd):
|
||||
self._published = os.system(cmd) == 0
|
||||
|
||||
def _publish(self):
|
||||
web_repo = os.getenv('YUBICO_GITHUB_REPO')
|
||||
if web_repo and os.path.isdir(web_repo):
|
||||
artifacts = [
|
||||
'dist/%s.tar.gz' % self.fullname,
|
||||
'dist/%s.tar.gz.sig' % self.fullname
|
||||
]
|
||||
cmd = '%s/publish %s %s %s' % (
|
||||
web_repo, self.name, self.version, ' '.join(artifacts))
|
||||
|
||||
self.execute(self._do_call_publish, (cmd,))
|
||||
if self._published:
|
||||
self.announce("Release published! Don't forget to:", log.INFO)
|
||||
self.announce("")
|
||||
self.announce(" (cd %s && git push)" % web_repo, log.INFO)
|
||||
self.announce("")
|
||||
else:
|
||||
self.warn("There was a problem publishing the release!")
|
||||
else:
|
||||
self.warn("YUBICO_GITHUB_REPO not set or invalid!")
|
||||
self.warn("This release will not be published!")
|
||||
|
||||
def run(self):
|
||||
if os.getcwd() != self.cwd:
|
||||
raise DistutilsSetupError("Must be in package root!")
|
||||
|
||||
self._verify_version()
|
||||
self._verify_tag()
|
||||
|
||||
self.execute(os.system, ('git2cl > ChangeLog',))
|
||||
|
||||
if not self.skip_tests:
|
||||
self.run_command('check')
|
||||
try:
|
||||
self.run_command('test')
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
raise DistutilsSetupError("There were test failures!")
|
||||
|
||||
self.run_command('sdist')
|
||||
|
||||
if self.pypi:
|
||||
cmd_obj = self.distribution.get_command_obj('upload')
|
||||
cmd_obj.sign = True
|
||||
if self.keyid:
|
||||
cmd_obj.identity = self.keyid
|
||||
self.run_command('upload')
|
||||
|
||||
self._sign()
|
||||
self._tag()
|
||||
|
||||
self._publish()
|
||||
|
||||
self.announce("Release complete! Don't forget to:", log.INFO)
|
||||
self.announce("")
|
||||
self.announce(" git push && git push --tags", log.INFO)
|
||||
self.announce("")
|
40
setup.py
Normal file
40
setup.py
Normal file
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup
|
||||
from release import release
|
||||
import re
|
||||
|
||||
VERSION_PATTERN = re.compile(r"(?m)^__version__\s*=\s*['\"](.+)['\"]$")
|
||||
|
||||
|
||||
def get_version():
|
||||
"""Return the current version as defined by yubico/yubico_version.py."""
|
||||
|
||||
with open('yubico/yubico_version.py', 'r') as f:
|
||||
match = VERSION_PATTERN.search(f.read())
|
||||
return match.group(1)
|
||||
|
||||
|
||||
setup(
|
||||
name='python-yubico',
|
||||
description='Python code for talking to Yubico\'s YubiKeys',
|
||||
version=get_version(),
|
||||
author='Dain Nilsson', # Original author: Fredrik Thulin
|
||||
author_email='dain@yubico.com',
|
||||
maintainer='Yubico Open Source Maintainers',
|
||||
maintainer_email='ossmaint@yubico.com',
|
||||
url='https://github.com/Yubico/python-yubico',
|
||||
license='BSD 2 clause',
|
||||
packages=['yubico'],
|
||||
install_requires=['pyusb'],
|
||||
test_suite='test',
|
||||
cmdclass={'release': release},
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
]
|
||||
)
|
2
test/__init__.py
Normal file
2
test/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
7
test/soft/__init__.py
Normal file
7
test/soft/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
"""
|
||||
Unit tests testing logic of the library.
|
||||
These do not require a physical YubiKey to run.
|
||||
"""
|
49
test/soft/test_yubico.py
Normal file
49
test/soft/test_yubico.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Simple test cases for a Python version of the yubikey_crc16() function in ykcrc.c.
|
||||
#
|
||||
|
||||
import struct
|
||||
import unittest
|
||||
import yubico.yubico_util as yubico_util
|
||||
from yubico.yubico_util import crc16
|
||||
|
||||
CRC_OK_RESIDUAL=0xf0b8
|
||||
|
||||
class TestCRC(unittest.TestCase):
|
||||
|
||||
def test_first(self):
|
||||
""" Test CRC16 trivial case """
|
||||
buffer = b'\x01\x02\x03\x04'
|
||||
crc = crc16(buffer)
|
||||
self.assertEqual(crc, 0xc66e)
|
||||
return buffer,crc
|
||||
|
||||
def test_second(self):
|
||||
""" Test CRC16 residual calculation """
|
||||
buffer,crc = self.test_first()
|
||||
# Append 1st complement for a "self-verifying" block -
|
||||
# from example in Yubikey low level interface
|
||||
crc_inv = 0xffff - crc
|
||||
buffer += struct.pack('<H', crc_inv)
|
||||
crc2 = crc16(buffer)
|
||||
self.assertEqual(crc2, CRC_OK_RESIDUAL)
|
||||
|
||||
def test_hexdump(self):
|
||||
""" Test hexdump function, normal use """
|
||||
bytes = b'\x01\x02\x03\x04\x05\x06\x07\x08'
|
||||
self.assertEqual(yubico_util.hexdump(bytes, length=4), \
|
||||
'0000 01 02 03 04\n0004 05 06 07 08\n')
|
||||
|
||||
def test_hexdump2(self):
|
||||
""" Test hexdump function, with colors """
|
||||
bytes = b'\x01\x02\x03\x04\x05\x06\x07\x08'
|
||||
self.assertEqual(yubico_util.hexdump(bytes, length=4, colorize=True), \
|
||||
'0000 \x1b[0m01 02 03\x1b[0m 04\n0004 \x1b[0m05 06 07\x1b[0m 08\n')
|
||||
|
||||
def test_modhex_decode(self):
|
||||
""" Test modhex decoding """
|
||||
self.assertEqual(b"0123456789abcdef", yubico_util.modhex_decode(b"cbdefghijklnrtuv"))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
307
test/soft/test_yubikey_config.py
Normal file
307
test/soft/test_yubikey_config.py
Normal file
@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
import yubico
|
||||
import yubico.yubikey_config
|
||||
from yubico.yubikey_usb_hid import YubiKeyConfigUSBHID
|
||||
import yubico.yubico_util
|
||||
import yubico.yubico_exception
|
||||
|
||||
class YubiKeyTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
version = (2, 2, 0,)
|
||||
capa = yubico.yubikey_usb_hid.YubiKeyUSBHIDCapabilities( \
|
||||
model = 'YubiKey', version = version, default_answer = False)
|
||||
self.Config = YubiKeyConfigUSBHID(ykver = version, capabilities = capa)
|
||||
|
||||
def test_static_ticket(self):
|
||||
""" Test static ticket """
|
||||
|
||||
#fixed: m:
|
||||
#uid: h:000000000000
|
||||
#key: h:e2bee9a36568a00d026a02f85e61e6fb
|
||||
#acc_code: h:000000000000
|
||||
#ticket_flags: APPEND_CR
|
||||
#config_flags: STATIC_TICKET
|
||||
|
||||
expected = [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\xe2\xbe\xe9\xa3\x65\x68\x83',
|
||||
b'\xa0\x0d\x02\x6a\x02\xf8\x5e\x84',
|
||||
b'\x61\xe6\xfb\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x00\x00\x20\x20\x00\x86',
|
||||
b'\x00\x5a\x93\x00\x00\x00\x00\x87',
|
||||
b'\x00\x01\x95\x56\x00\x00\x00\x89'
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
Config.aes_key(b'h:e2bee9a36568a00d026a02f85e61e6fb')
|
||||
Config.ticket_flag('APPEND_CR', True)
|
||||
Config.config_flag('STATIC_TICKET', True)
|
||||
|
||||
data = Config.to_frame(slot=1).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
|
||||
def test_static_ticket_with_access_code(self):
|
||||
""" Test static ticket with unlock code """
|
||||
|
||||
#fixed: m:
|
||||
#uid: h:000000000000
|
||||
#key: h:e2bee9a36568a00d026a02f85e61e6fb
|
||||
#acc_code: h:010203040506
|
||||
#ticket_flags: APPEND_CR
|
||||
#config_flags: STATIC_TICKET
|
||||
|
||||
expected = [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\xe2\xbe\xe9\xa3\x65\x68\x83',
|
||||
b'\xa0\x0d\x02\x6a\x02\xf8\x5e\x84',
|
||||
b'\x61\xe6\xfb\x01\x02\x03\x04\x85',
|
||||
b'\x05\x06\x00\x00\x20\x20\x00\x86',
|
||||
b'\x00\x0d\x39\x01\x02\x03\x04\x87',
|
||||
b'\x05\x06\x00\x00\x00\x00\x00\x88',
|
||||
b'\x00\x01\xc2\xfc\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
Config.aes_key(b'h:e2bee9a36568a00d026a02f85e61e6fb')
|
||||
Config.ticket_flag('APPEND_CR', True)
|
||||
Config.config_flag('STATIC_TICKET', True)
|
||||
Config.unlock_key(b'h:010203040506')
|
||||
|
||||
data = Config.to_frame(slot=1).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_fixed_and_oath_hotp(self):
|
||||
""" Test OATH HOTP with a fixed prefix-string """
|
||||
|
||||
#fixed: m:ftftftft
|
||||
#uid: h:000000000000
|
||||
#key: h:523d7ce7e7b6ee853517a3e3cc1985c7
|
||||
#acc_code: h:000000000000
|
||||
#ticket_flags: APPEND_CR|OATH_HOTP
|
||||
#config_flags: OATH_FIXED_MODHEX1|OATH_FIXED_MODHEX2|STATIC_TICKET
|
||||
|
||||
expected = [b'\x4d\x4d\x4d\x4d\x00\x00\x00\x80',
|
||||
b'\x00\x52\x3d\x7c\xe7\xe7\xb6\x83',
|
||||
b'\xee\x85\x35\x17\xa3\xe3\xcc\x84',
|
||||
b'\x19\x85\xc7\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x04\x00\x60\x70\x00\x86',
|
||||
b'\x00\x72\xad\xaa\xbb\xcc\xdd\x87',
|
||||
b'\xee\xff\x00\x00\x00\x00\x00\x88',
|
||||
b'\x00\x03\xfe\xc4\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
Config.aes_key(b'h:523d7ce7e7b6ee853517a3e3cc1985c7')
|
||||
Config.fixed_string(b'm:ftftftft')
|
||||
Config.ticket_flag('APPEND_CR', True)
|
||||
Config.ticket_flag('OATH_HOTP', True)
|
||||
Config.config_flag('OATH_FIXED_MODHEX1', True)
|
||||
Config.config_flag('OATH_FIXED_MODHEX2', True)
|
||||
Config.config_flag('STATIC_TICKET', True)
|
||||
Config.unlock_key(b'h:aabbccddeeff')
|
||||
Config.access_key(b'h:000000000000')
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_challenge_response_hmac_nist(self):
|
||||
""" Test HMAC challenge response with NIST test vector """
|
||||
|
||||
expected = [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\x00\x40\x41\x42\x43\x00\x82',
|
||||
b'\x00\x30\x31\x32\x33\x34\x35\x83',
|
||||
b'\x36\x37\x38\x39\x3a\x3b\x3c\x84',
|
||||
b'\x3d\x3e\x3f\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x00\x04\x40\x26\x00\x86',
|
||||
b'\x00\x98\x41\x00\x00\x00\x00\x87',
|
||||
b'\x00\x03\x95\x56\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Config.mode_challenge_response(secret, type='HMAC', variable=True)
|
||||
Config.extended_flag('SERIAL_API_VISIBLE', True)
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_unknown_ticket_flag(self):
|
||||
""" Test setting unknown ticket flag """
|
||||
Config = self.Config
|
||||
self.assertRaises(yubico.yubico_exception.InputError, Config.ticket_flag, 'YK_UNIT_TEST123', True)
|
||||
|
||||
def test_unknown_ticket_flag_integer(self):
|
||||
""" Test setting unknown ticket flag as integer """
|
||||
future_flag = 0xff
|
||||
Config = self.Config
|
||||
Config.ticket_flag(future_flag, True)
|
||||
self.assertEqual(future_flag, Config.ticket_flags.to_integer())
|
||||
|
||||
def test_too_long_fixed_string(self):
|
||||
""" Test too long fixed string, and set as plain string """
|
||||
Config = self.Config
|
||||
self.assertRaises(yubico.yubico_exception.InputError, Config.ticket_flag, 'YK_UNIT_TEST123', True)
|
||||
|
||||
def test_default_flags(self):
|
||||
""" Test that no flags get set by default """
|
||||
Config = self.Config
|
||||
self.assertEqual(0x0, Config.ticket_flags.to_integer())
|
||||
self.assertEqual(0x0, Config.config_flags.to_integer())
|
||||
self.assertEqual(0x0, Config.extended_flags.to_integer())
|
||||
|
||||
def test_oath_hotp_like_windows(self):
|
||||
""" Test plain OATH-HOTP with NIST test vector """
|
||||
|
||||
expected = [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\x00\x40\x41\x42\x43\x00\x82',
|
||||
b'\x00\x30\x31\x32\x33\x34\x35\x83',
|
||||
b'\x36\x37\x38\x39\x3a\x3b\x3c\x84',
|
||||
b'\x3d\x3e\x3f\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x00\x00\x40\x00\x00\x86',
|
||||
b'\x00\x6a\xb9\x00\x00\x00\x00\x87',
|
||||
b'\x00\x03\x95\x56\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Config.mode_oath_hotp(secret)
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_oath_hotp_like_windows2(self):
|
||||
""" Test OATH-HOTP with NIST test vector and token identifier """
|
||||
|
||||
expected = [b'\x01\x02\x03\x04\x05\x06\x00\x80',
|
||||
b'\x00\x00\x40\x41\x42\x43\x00\x82',
|
||||
b'\x00\x30\x31\x32\x33\x34\x35\x83',
|
||||
b'\x36\x37\x38\x39\x3a\x3b\x3c\x84',
|
||||
b'\x3d\x3e\x3f\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x06\x00\x40\x42\x00\x86',
|
||||
b'\x00\x0e\xec\x00\x00\x00\x00\x87',
|
||||
b'\x00\x03\x95\x56\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Config.mode_oath_hotp(secret, digits=8, factor_seed='', omp=0x01, tt=0x02, mui=b'\x03\x04\x05\x06')
|
||||
Config.config_flag('OATH_FIXED_MODHEX2', True)
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_oath_hotp_like_windows_factory_seed(self):
|
||||
""" Test OATH-HOTP factor_seed """
|
||||
|
||||
expected = [b'\x01\x02\x03\x04\x05\x06\x00\x80',
|
||||
b'\x00\x00\x40\x41\x42\x43\x01\x82',
|
||||
b'\x21\x30\x31\x32\x33\x34\x35\x83',
|
||||
b'\x36\x37\x38\x39\x3a\x3b\x3c\x84',
|
||||
b'\x3d\x3e\x3f\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x06\x00\x40\x42\x00\x86',
|
||||
b'\x00\x03\xea\x00\x00\x00\x00\x87',
|
||||
b'\x00\x03\x95\x56\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Config.mode_oath_hotp(secret, digits=8, factor_seed=0x2101, omp=0x01, tt=0x02, mui=b'\x03\x04\x05\x06')
|
||||
Config.config_flag('OATH_FIXED_MODHEX2', True)
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_fixed_length_hmac_like_windows(self):
|
||||
""" Test fixed length HMAC SHA1 """
|
||||
|
||||
expected = [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\x00\x40\x41\x42\x43\x00\x82',
|
||||
b'\x00\x30\x31\x32\x33\x34\x35\x83',
|
||||
b'\x36\x37\x38\x39\x3a\x3b\x3c\x84',
|
||||
b'\x3d\x3e\x3f\x00\x00\x00\x00\x85',
|
||||
b'\x00\x00\x00\x00\x40\x22\x00\x86',
|
||||
b'\x00\xe9\x0f\x00\x00\x00\x00\x87',
|
||||
b'\x00\x03\x95\x56\x00\x00\x00\x89',
|
||||
]
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
Config.mode_challenge_response(secret, type='HMAC', variable=False)
|
||||
|
||||
data = Config.to_frame(slot=2).to_feature_reports()
|
||||
|
||||
print("EXPECT:\n%s\nGOT:\n%s\n" %( yubico.yubico_util.hexdump(b''.join(expected)),
|
||||
yubico.yubico_util.hexdump(b''.join(data))))
|
||||
|
||||
self.assertEqual(data, expected)
|
||||
|
||||
def test_version_required_1(self):
|
||||
""" Test YubiKey 1 with v2 option """
|
||||
version = (1, 3, 0,)
|
||||
capa = yubico.yubikey_usb_hid.YubiKeyUSBHIDCapabilities( \
|
||||
model = 'YubiKey', version = version, default_answer = False)
|
||||
Config = YubiKeyConfigUSBHID(ykver = version, capabilities = capa)
|
||||
self.assertRaises(yubico.yubikey.YubiKeyVersionError, Config.config_flag, 'SHORT_TICKET', True)
|
||||
|
||||
def test_version_required_2(self):
|
||||
""" Test YubiKey 2 with v2 option """
|
||||
|
||||
Config = self.Config
|
||||
Config.config_flag('SHORT_TICKET', True)
|
||||
self.assertEqual((2, 0), Config.version_required())
|
||||
|
||||
def test_version_required_3(self):
|
||||
""" Test YubiKey 2 with v1 option """
|
||||
|
||||
Config = self.Config
|
||||
self.assertRaises(yubico.yubikey.YubiKeyVersionError, Config.config_flag, 'TICKET_FIRST', True)
|
||||
|
||||
def test_version_required_4(self):
|
||||
""" Test YubiKey 2.1 with v2.2 mode """
|
||||
version = (2, 1, 0,)
|
||||
capa = yubico.yubikey_usb_hid.YubiKeyUSBHIDCapabilities( \
|
||||
model = 'YubiKey', version = version, default_answer = False)
|
||||
Config = YubiKeyConfigUSBHID(ykver = version, capabilities = capa)
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f40414243'
|
||||
self.assertRaises(yubico.yubikey.YubiKeyVersionError, Config.mode_challenge_response, secret)
|
||||
|
||||
def test_version_required_5(self):
|
||||
""" Test YubiKey 2.2 with v2.2 mode """
|
||||
|
||||
Config = self.Config
|
||||
secret = b'h:303132333435363738393a3b3c3d3e3f'
|
||||
Config.mode_challenge_response(secret, type='OTP')
|
||||
self.assertEqual('CHAL_RESP', Config._mode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
58
test/soft/test_yubikey_frame.py
Normal file
58
test/soft/test_yubikey_frame.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from yubico import *
|
||||
from yubico.yubikey_frame import *
|
||||
import yubico.yubico_exception
|
||||
import unittest
|
||||
import struct
|
||||
import re
|
||||
|
||||
class YubiKeyTests(unittest.TestCase):
|
||||
|
||||
def test_get_ykframe(self):
|
||||
""" Test normal use """
|
||||
buffer = YubiKeyFrame(command=0x01).to_string()
|
||||
|
||||
# check number of bytes returned
|
||||
self.assertEqual(len(buffer), 70, "yubikey command buffer should always be 70 bytes")
|
||||
|
||||
# check that empty payload works (64 * '\x00')
|
||||
all_zeros = b'\x00' * 64
|
||||
|
||||
self.assertTrue(buffer.startswith(all_zeros))
|
||||
|
||||
|
||||
def test_get_ykframe_feature_reports(self):
|
||||
""" Test normal use """
|
||||
res = YubiKeyFrame(command=0x32).to_feature_reports()
|
||||
|
||||
self.assertEqual(res, [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\x32\x6b\x5b\x00\x00\x00\x89'
|
||||
])
|
||||
|
||||
|
||||
def test_get_ykframe_feature_reports2(self):
|
||||
""" Test one serie of non-zero bytes in the middle of the payload """
|
||||
payload = b'\x00' * 38
|
||||
payload += b'\x01\x02\x03'
|
||||
payload += b'\x00' * 23
|
||||
res = YubiKeyFrame(command=0x32, payload=payload).to_feature_reports()
|
||||
|
||||
self.assertEqual(res, [b'\x00\x00\x00\x00\x00\x00\x00\x80',
|
||||
b'\x00\x00\x00\x01\x02\x03\x00\x85',
|
||||
b'\x002\x01s\x00\x00\x00\x89'])
|
||||
|
||||
def test_bad_payload(self):
|
||||
""" Test that we get an exception for four bytes payload """
|
||||
self.assertRaises(yubico_exception.InputError, YubiKeyFrame, command=0x32, payload=b'test')
|
||||
|
||||
def test_repr(self):
|
||||
""" Test string representation of object """
|
||||
# to achieve 100% test coverage ;)
|
||||
frame = YubiKeyFrame(command=0x4d)
|
||||
print("Frame is represented as %s" % frame)
|
||||
re_match = re.search("YubiKeyFrame instance at .*: 77.$", str(frame))
|
||||
self.assertNotEqual(re_match, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
7
test/usb/__init__.py
Normal file
7
test/usb/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# Copyright (c) 2010, 2011, 2012 Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
|
||||
"""
|
||||
Tests that run against a physical YubiKey.
|
||||
These tests require an attached YubiKey that has been correctly configured.
|
||||
"""
|
56
test/usb/test_yubikey_usb_hid.py
Normal file
56
test/usb/test_yubikey_usb_hid.py
Normal file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Test cases for talking to a USB HID YubiKey.
|
||||
#
|
||||
|
||||
import struct
|
||||
import unittest
|
||||
import yubico
|
||||
import yubico.yubikey_usb_hid
|
||||
from yubico.yubikey_usb_hid import *
|
||||
import re
|
||||
|
||||
class TestYubiKeyUSBHID(unittest.TestCase):
|
||||
|
||||
YK = None
|
||||
|
||||
def setUp(self):
|
||||
""" Test connecting to the YubiKey """
|
||||
if self.YK is None:
|
||||
try:
|
||||
print("open key")
|
||||
self.YK = YubiKeyUSBHID()
|
||||
return
|
||||
except YubiKeyUSBHIDError as err:
|
||||
self.fail("No YubiKey connected (?) : %s" % str(err))
|
||||
|
||||
def tearDown(self):
|
||||
if self.YK is not None:
|
||||
del self.YK
|
||||
|
||||
#@unittest.skipIf(YK is None, "No USB HID YubiKey found")
|
||||
def test_status(self):
|
||||
""" Test the simplest form of communication : a status read request """
|
||||
status = self.YK.status()
|
||||
version = self.YK.version()
|
||||
print("Version returned: %s" % version)
|
||||
re_match = re.match("\d+\.\d+\.\d+$", version)
|
||||
self.assertNotEqual(re_match, None)
|
||||
|
||||
#@unittest.skipIf(self.YK is None, "No USB HID YubiKey found")
|
||||
def test_challenge_response(self):
|
||||
""" Test challenge-response, assumes a NIST PUB 198 A.2 20 bytes test vector in Slot 2 (variable input) """
|
||||
|
||||
secret = struct.pack('64s', b'Sample #2')
|
||||
response = self.YK.challenge_response(secret, mode='HMAC', slot=2)
|
||||
self.assertEqual(response, b'\x09\x22\xd3\x40\x5f\xaa\x3d\x19\x4f\x82\xa4\x58\x30\x73\x7d\x5c\xc6\xc7\x5d\x24')
|
||||
|
||||
#@unittest.skipIf(self.YK is None, "No USB HID YubiKey found")
|
||||
def test_serial(self):
|
||||
""" Test serial number retrieval (requires YubiKey 2) """
|
||||
serial = self.YK.serial()
|
||||
print("Serial returned : %s" % serial)
|
||||
self.assertEqual(type(serial), type(1))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
132
util/yubikey-totp
Executable file
132
util/yubikey-totp
Executable file
@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2011, Yubico AB
|
||||
# See the file COPYING for licence statement.
|
||||
#
|
||||
|
||||
"""
|
||||
This program lets you use the HMAC-SHA-1 in your YubiKey to produce
|
||||
OATH TOTP (RFC 6238) codes.
|
||||
|
||||
To verify the output of this program, first program a YubiKey with the
|
||||
RFC 6238 test key "12345678901234567890" (ASCII) :
|
||||
|
||||
$ ykpersonalize -2 -ochal-resp -ochal-hmac -ohmac-lt64 \
|
||||
-o serial-api-visible \
|
||||
-a 3132333435363738393031323334353637383930
|
||||
|
||||
and then examine the OATH codes for the test values (Time) in Appendix B
|
||||
of RFC 6238 (SHA1) :
|
||||
|
||||
Time SHA1
|
||||
59 -> 94287082
|
||||
1111111109 -> 07081804
|
||||
1234567890 -> 89005924
|
||||
20000000000 -> 65353130
|
||||
|
||||
Like this :
|
||||
|
||||
$ yubikey-totp --step 30 --digits 8 --time 59
|
||||
94287082
|
||||
$
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
import struct
|
||||
import yubico
|
||||
import argparse
|
||||
import binascii
|
||||
|
||||
default_slot=2
|
||||
default_time=int(time.time())
|
||||
default_step=30
|
||||
default_digits=6
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Parse the command line arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description = "Generate OATH TOTP codes using a YubiKey",
|
||||
add_help = True,
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument('-v', '--verbose',
|
||||
dest='verbose',
|
||||
action='store_true', default=False,
|
||||
help='Enable verbose operation'
|
||||
)
|
||||
parser.add_argument('--debug',
|
||||
dest='debug',
|
||||
action='store_true', default=False,
|
||||
help='Enable debug operation'
|
||||
)
|
||||
parser.add_argument('--time',
|
||||
dest='time',
|
||||
type=int, default=default_time,
|
||||
required=False,
|
||||
help='Time to use as number of seconds since epoch',
|
||||
)
|
||||
parser.add_argument('--step',
|
||||
dest='step',
|
||||
type=int, default=default_step,
|
||||
required=False,
|
||||
help='Time step in use (in seconds)',
|
||||
)
|
||||
parser.add_argument('--digits',
|
||||
dest='digits',
|
||||
type=int, default=default_digits,
|
||||
required=False,
|
||||
help='Length of OTP in decimal digits',
|
||||
)
|
||||
parser.add_argument('--slot',
|
||||
dest='slot',
|
||||
type=int, default=default_slot,
|
||||
required=False,
|
||||
help='YubiKey slot configured for Challenge-Response',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
def make_totp(args):
|
||||
"""
|
||||
Create an OATH TOTP OTP and return it as a string (to disambiguate leading zeros).
|
||||
"""
|
||||
YK = yubico.find_yubikey(debug=args.debug)
|
||||
if args.debug or args.verbose:
|
||||
print("Version : %s " % YK.version())
|
||||
if args.debug:
|
||||
print("Serial : %i" % YK.serial())
|
||||
print("")
|
||||
# Do challenge-response
|
||||
secret = struct.pack("> Q", args.time / args.step).ljust(64, chr(0x0))
|
||||
if args.debug:
|
||||
print("Sending challenge : %s\n" % (binascii.hexlify(secret)))
|
||||
response = YK.challenge_response(secret, slot=args.slot)
|
||||
# format with appropriate number of leading zeros
|
||||
totp_str = '%.*i' % (args.digits, yubico.yubico_util.hotp_truncate(response, length=args.digits))
|
||||
return totp_str
|
||||
|
||||
def main():
|
||||
""" Main program. """
|
||||
args = parse_args()
|
||||
|
||||
otp = None
|
||||
try:
|
||||
otp = make_totp(args)
|
||||
except yubico.yubico_exception.YubicoError as e:
|
||||
print("ERROR: %s" % (e.reason))
|
||||
return 1
|
||||
|
||||
if not otp:
|
||||
return 1
|
||||
|
||||
print(otp)
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
102
util/yubikey-totp.1
Normal file
102
util/yubikey-totp.1
Normal file
@ -0,0 +1,102 @@
|
||||
.\" Copyright (c) 2012 Yubico AB
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
.\" Redistribution and use in source and binary forms, with or without
|
||||
.\" modification, are permitted provided that the following conditions are
|
||||
.\" met:
|
||||
.\"
|
||||
.\" * Redistributions of source code must retain the above copyright
|
||||
.\" notice, this list of conditions and the following disclaimer.
|
||||
.\"
|
||||
.\" * Redistributions in binary form must reproduce the above
|
||||
.\" copyright notice, this list of conditions and the following
|
||||
.\" disclaimer in the documentation and/or other materials provided
|
||||
.\" with the distribution.
|
||||
.\"
|
||||
.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
.\"
|
||||
.\" The following commands are required for all man pages.
|
||||
.de URL
|
||||
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||
..
|
||||
.if \n[.g] .mso www.tmac
|
||||
.TH yubikey-totp "1" "June 2012" "python-yubico"
|
||||
.SH NAME
|
||||
yubikey-totp - Produce an OATH TOTP code using a YubiKey
|
||||
.SH SYNOPSIS
|
||||
.B yubikey-totp
|
||||
[\fI-v\fR] [\fI-h\fR] [\fI--time\fR | \fI--step\fR] [\fI--digits\fR] [\fI--slot\fR] [\fI--debug\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
OATH codes are one time passwords (OTP) calculated in a standardized way. While the YubiKey
|
||||
is primarily used with Yubico OTP's, the YubiKey is also capable of producing OATH codes.
|
||||
|
||||
OATH generally comes in two flavors -- event based (called HOTP) and time based (called TOTP).
|
||||
Since the YubiKey does not contain a battery, it cannot keep track of the current time itself
|
||||
and therefor a helper application such as yubikey-totp is required to effectively send the
|
||||
current time to the YubiKey, which can then perform the cryptographic calculation needed to
|
||||
produce the OATH code.
|
||||
|
||||
Through the use of a helper application, such as yubikey-totp, the YubiKey can be used with
|
||||
sites offering OATH TOTP authentication, such as Google GMail.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-v\fR
|
||||
enable verbose mode.
|
||||
.TP
|
||||
\fB\-h\fR
|
||||
show help
|
||||
.TP
|
||||
\fB\-\-time\fR
|
||||
specify the time value to use (in seconds since epoch)
|
||||
.TP
|
||||
\fB\-\-step\fR
|
||||
how frequent codes change in your system - typically 30 or 60 seconds
|
||||
.TP
|
||||
\fB\-\-digits\fR
|
||||
digits in OATH code - typically 6
|
||||
.TP
|
||||
\fB\-\-slot\fR
|
||||
YubiKey slot to use - default 2
|
||||
.TP
|
||||
\fB\-\-debug\fR
|
||||
enable debug output
|
||||
|
||||
.SH EXAMPLE
|
||||
|
||||
The YubiKey OATH TOTP operation can be demonstrated using the
|
||||
\fBRFC 6238\fR test key "12345678901234567890" (ASCII).
|
||||
.P
|
||||
First, program a YubiKey for HMAC-SHA1 Challenge-Response operation with the test vector HMAC key :
|
||||
.HP
|
||||
.nf
|
||||
$ \fBykpersonalize \-2 \-ochal\-resp \-ochal\-hmac \-ohmac\-lt64 \-o serial\-api\-visible \\
|
||||
\-a 3132333435363738393031323334353637383930\fR
|
||||
.fi
|
||||
.HP
|
||||
Now, send the NIST test challenge to the YubiKey and verify the result matches the
|
||||
expected :
|
||||
.HP
|
||||
.nf
|
||||
$ \fByubikey\-totp \-\-step 30 \-\-digits 8 \-\-time 1111111109\fR
|
||||
07081804
|
||||
$
|
||||
.fi
|
||||
|
||||
.SH BUGS
|
||||
Report yubikey-totp bugs in
|
||||
.URL "https://github.com/Yubico/python-yubico/issues/" "the issue tracker" "."
|
||||
.SH "SEE ALSO"
|
||||
.PP
|
||||
YubiKeys can be obtained from
|
||||
.URL "http://www.yubico.com/" "Yubico" "."
|
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)
|
Loading…
x
Reference in New Issue
Block a user