commit 65be73d223579d6d0ce7efa17564cd77e16157f6 Author: geos_one Date: Wed Aug 13 10:23:20 2025 +0200 Import Upstream version 1.3.3 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..2a66fc5 --- /dev/null +++ b/COPYING @@ -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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4b6aead --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include release.py +include COPYING +include NEWS +include ChangeLog +include examples/* +include util/* +include doc/* +recursive-include test *.py diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..853b0a4 --- /dev/null +++ b/NEWS @@ -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. diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..f7354f1 --- /dev/null +++ b/PKG-INFO @@ -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 diff --git a/README b/README new file mode 100644 index 0000000..b4b1a4e --- /dev/null +++ b/README @@ -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. diff --git a/doc/ykdef.h b/doc/ykdef.h new file mode 100644 index 0000000..2eced9e --- /dev/null +++ b/doc/ykdef.h @@ -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__ */ diff --git a/examples/configure_neo_ndef b/examples/configure_neo_ndef new file mode 100755 index 0000000..44fc87d --- /dev/null +++ b/examples/configure_neo_ndef @@ -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) diff --git a/examples/configure_nist_test_key b/examples/configure_nist_test_key new file mode 100755 index 0000000..ae0dd22 --- /dev/null +++ b/examples/configure_nist_test_key @@ -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) diff --git a/examples/nist_challenge_response b/examples/nist_challenge_response new file mode 100755 index 0000000..f5ea188 --- /dev/null +++ b/examples/nist_challenge_response @@ -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) diff --git a/examples/rolling_challenge_response b/examples/rolling_challenge_response new file mode 100755 index 0000000..c371392 --- /dev/null +++ b/examples/rolling_challenge_response @@ -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() diff --git a/examples/update_cfg_remove_cr b/examples/update_cfg_remove_cr new file mode 100755 index 0000000..78012b3 --- /dev/null +++ b/examples/update_cfg_remove_cr @@ -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) diff --git a/examples/yubikey-inventory b/examples/yubikey-inventory new file mode 100755 index 0000000..e36414d --- /dev/null +++ b/examples/yubikey-inventory @@ -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 diff --git a/python_yubico.egg-info/PKG-INFO b/python_yubico.egg-info/PKG-INFO new file mode 100644 index 0000000..f7354f1 --- /dev/null +++ b/python_yubico.egg-info/PKG-INFO @@ -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 diff --git a/python_yubico.egg-info/SOURCES.txt b/python_yubico.egg-info/SOURCES.txt new file mode 100644 index 0000000..90b7f4e --- /dev/null +++ b/python_yubico.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/python_yubico.egg-info/dependency_links.txt b/python_yubico.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python_yubico.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/python_yubico.egg-info/requires.txt b/python_yubico.egg-info/requires.txt new file mode 100644 index 0000000..6513d5e --- /dev/null +++ b/python_yubico.egg-info/requires.txt @@ -0,0 +1 @@ +pyusb diff --git a/python_yubico.egg-info/top_level.txt b/python_yubico.egg-info/top_level.txt new file mode 100644 index 0000000..0ad4a6b --- /dev/null +++ b/python_yubico.egg-info/top_level.txt @@ -0,0 +1 @@ +yubico diff --git a/release.py b/release.py new file mode 100644 index 0000000..1f70b22 --- /dev/null +++ b/release.py @@ -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("") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e120c08 --- /dev/null +++ b/setup.py @@ -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', + ] +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..22790cd --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2010, 2011, 2012 Yubico AB +# See the file COPYING for licence statement. diff --git a/test/soft/__init__.py b/test/soft/__init__.py new file mode 100644 index 0000000..6c45b9c --- /dev/null +++ b/test/soft/__init__.py @@ -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. +""" diff --git a/test/soft/test_yubico.py b/test/soft/test_yubico.py new file mode 100644 index 0000000..de2e2a5 --- /dev/null +++ b/test/soft/test_yubico.py @@ -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(' 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()) diff --git a/util/yubikey-totp.1 b/util/yubikey-totp.1 new file mode 100644 index 0000000..19be760 --- /dev/null +++ b/util/yubikey-totp.1 @@ -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" "." diff --git a/yubico/__init__.py b/yubico/__init__.py new file mode 100644 index 0000000..d81c1b1 --- /dev/null +++ b/yubico/__init__.py @@ -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 diff --git a/yubico/yubico_exception.py b/yubico/yubico_exception.py new file mode 100644 index 0000000..b829f0e --- /dev/null +++ b/yubico/yubico_exception.py @@ -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) diff --git a/yubico/yubico_util.py b/yubico/yubico_util.py new file mode 100644 index 0000000..1f583f0 --- /dev/null +++ b/yubico/yubico_util.py @@ -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 diff --git a/yubico/yubico_version.py b/yubico/yubico_version.py new file mode 100644 index 0000000..7b1e312 --- /dev/null +++ b/yubico/yubico_version.py @@ -0,0 +1 @@ +__version__ = "1.3.3" diff --git a/yubico/yubikey.py b/yubico/yubikey.py new file mode 100644 index 0000000..807a236 --- /dev/null +++ b/yubico/yubikey.py @@ -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 diff --git a/yubico/yubikey_4_usb_hid.py b/yubico/yubikey_4_usb_hid.py new file mode 100644 index 0000000..b44bb90 --- /dev/null +++ b/yubico/yubikey_4_usb_hid.py @@ -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] diff --git a/yubico/yubikey_base.py b/yubico/yubikey_base.py new file mode 100644 index 0000000..a456e53 --- /dev/null +++ b/yubico/yubikey_base.py @@ -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 diff --git a/yubico/yubikey_config.py b/yubico/yubikey_config.py new file mode 100644 index 0000000..b5a30c4 --- /dev/null +++ b/yubico/yubikey_config.py @@ -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(' 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() diff --git a/yubico/yubikey_config_util.py b/yubico/yubikey_config_util.py new file mode 100644 index 0000000..f43ccb6 --- /dev/null +++ b/yubico/yubikey_config_util.py @@ -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) diff --git a/yubico/yubikey_defs.py b/yubico/yubikey_defs.py new file mode 100644 index 0000000..2821e61 --- /dev/null +++ b/yubico/yubikey_defs.py @@ -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 diff --git a/yubico/yubikey_frame.py b/yubico/yubikey_frame.py new file mode 100644 index 0000000..47ef0e7 --- /dev/null +++ b/yubico/yubikey_frame.py @@ -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, '') diff --git a/yubico/yubikey_neo_usb_hid.py b/yubico/yubikey_neo_usb_hid.py new file mode 100644 index 0000000..0f4334a --- /dev/null +++ b/yubico/yubikey_neo_usb_hid.py @@ -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('= (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 = '= (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)