python-yubico-1.3.3/0000755000175000017500000000000013435740771014212 5ustar daindain00000000000000python-yubico-1.3.3/ChangeLog0000644000175000017500000000000013435740770015751 0ustar daindain00000000000000python-yubico-1.3.3/COPYING0000644000175000017500000000246613435565573015262 0ustar daindain00000000000000Copyright (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. python-yubico-1.3.3/doc/0000755000175000017500000000000013435740770014756 5ustar daindain00000000000000python-yubico-1.3.3/doc/ykdef.h0000644000175000017500000002314013435565573016237 0ustar daindain00000000000000/* -*- 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__ */ python-yubico-1.3.3/examples/0000755000175000017500000000000013435740770016027 5ustar daindain00000000000000python-yubico-1.3.3/examples/configure_neo_ndef0000755000175000017500000000146313435713206021571 0ustar daindain00000000000000#!/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) python-yubico-1.3.3/examples/configure_nist_test_key0000755000175000017500000000145613435713206022702 0ustar daindain00000000000000#!/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) python-yubico-1.3.3/examples/nist_challenge_response0000755000175000017500000000235113435565573022661 0ustar daindain00000000000000#!/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) python-yubico-1.3.3/examples/rolling_challenge_response0000755000175000017500000002162713435713206023345 0ustar daindain00000000000000#!/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() python-yubico-1.3.3/examples/update_cfg_remove_cr0000755000175000017500000000226513435713206022116 0ustar daindain00000000000000#!/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) python-yubico-1.3.3/examples/yubikey-inventory0000755000175000017500000000150213435565573021475 0ustar daindain00000000000000#!/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 python-yubico-1.3.3/MANIFEST.in0000644000175000017500000000021613435565573015754 0ustar daindain00000000000000include release.py include COPYING include NEWS include ChangeLog include examples/* include util/* include doc/* recursive-include test *.py python-yubico-1.3.3/NEWS0000644000175000017500000000207613435740743014715 0ustar daindain00000000000000* 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. python-yubico-1.3.3/PKG-INFO0000644000175000017500000000121313435740771015304 0ustar daindain00000000000000Metadata-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 python-yubico-1.3.3/python_yubico.egg-info/0000755000175000017500000000000013435740770020576 5ustar daindain00000000000000python-yubico-1.3.3/python_yubico.egg-info/dependency_links.txt0000644000175000017500000000000113435740770024644 0ustar daindain00000000000000 python-yubico-1.3.3/python_yubico.egg-info/PKG-INFO0000644000175000017500000000121313435740770021670 0ustar daindain00000000000000Metadata-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 python-yubico-1.3.3/python_yubico.egg-info/requires.txt0000644000175000017500000000000613435740770023172 0ustar daindain00000000000000pyusb python-yubico-1.3.3/python_yubico.egg-info/SOURCES.txt0000644000175000017500000000174313435740770022467 0ustar daindain00000000000000COPYING 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.pypython-yubico-1.3.3/python_yubico.egg-info/top_level.txt0000644000175000017500000000000713435740770023325 0ustar daindain00000000000000yubico python-yubico-1.3.3/README0000644000175000017500000000527613435565573015111 0ustar daindain00000000000000== 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. python-yubico-1.3.3/release.py0000644000175000017500000001311113435565573016206 0ustar daindain00000000000000# 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("") python-yubico-1.3.3/setup.cfg0000644000175000017500000000004613435740771016033 0ustar daindain00000000000000[egg_info] tag_build = tag_date = 0 python-yubico-1.3.3/setup.py0000644000175000017500000000230613435565573015732 0ustar daindain00000000000000#!/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', ] ) python-yubico-1.3.3/test/0000755000175000017500000000000013435740770015170 5ustar daindain00000000000000python-yubico-1.3.3/test/soft/0000755000175000017500000000000013435740770016143 5ustar daindain00000000000000python-yubico-1.3.3/test/soft/test_yubico.py0000644000175000017500000000317113435565573021056 0ustar daindain00000000000000#!/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()) python-yubico-1.3.3/util/yubikey-totp.10000644000175000017500000000717013435565573017730 0ustar daindain00000000000000.\" 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" "." python-yubico-1.3.3/yubico/0000755000175000017500000000000013435740771015504 5ustar daindain00000000000000python-yubico-1.3.3/yubico/yubico_exception.py0000644000175000017500000000235113435565573021434 0ustar daindain00000000000000""" 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) python-yubico-1.3.3/yubico/yubico_util.py0000644000175000017500000001057213435565573020417 0ustar daindain00000000000000""" 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 python-yubico-1.3.3/yubico/yubico_version.py0000644000175000017500000000002613435566013021106 0ustar daindain00000000000000__version__ = "1.3.3" python-yubico-1.3.3/yubico/yubikey.py0000644000175000017500000000416213435565573017547 0ustar daindain00000000000000""" 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 python-yubico-1.3.3/yubico/yubikey_4_usb_hid.py0000644000175000017500000000675513435565573021501 0ustar daindain00000000000000""" 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] python-yubico-1.3.3/yubico/yubikey_base.py0000644000175000017500000001331113435565573020535 0ustar daindain00000000000000""" 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 python-yubico-1.3.3/yubico/yubikey_config.py0000644000175000017500000005652713435565573021110 0ustar daindain00000000000000""" 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() python-yubico-1.3.3/yubico/yubikey_config_util.py0000644000175000017500000001166113435565573022133 0ustar daindain00000000000000""" 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) python-yubico-1.3.3/yubico/yubikey_defs.py0000644000175000017500000001506513435565573020554 0ustar daindain00000000000000""" 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 python-yubico-1.3.3/yubico/yubikey_frame.py0000644000175000017500000001123713435565573020722 0ustar daindain00000000000000""" 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, '') python-yubico-1.3.3/yubico/yubikey_neo_usb_hid.py0000644000175000017500000002475413435565573022116 0ustar daindain00000000000000""" 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) python-yubico-1.3.3/yubico/__init__.py0000644000175000017500000000170513435565573017625 0ustar daindain00000000000000""" 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