Import Upstream version 1.3.3
This commit is contained in:
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)
|
||||||
Reference in New Issue
Block a user