python3-yubico/yubico/yubikey_neo_usb_hid.py
2025-08-13 10:23:20 +02:00

351 lines
10 KiB
Python

"""
module for accessing a USB HID YubiKey NEO
"""
# Copyright (c) 2012 Yubico AB
# See the file COPYING for licence statement.
__all__ = [
# constants
'uri_identifiers',
# functions
# classes
'YubiKeyNEO_USBHID',
'YubiKeyNEO_USBHIDError'
]
import struct
import binascii
from .yubico_version import __version__
from .yubikey_defs import SLOT, MODE
from . import yubikey_usb_hid
from . import yubikey_base
from . import yubikey_frame
from . import yubico_exception
from . import yubico_util
# commands from ykdef.h
_ACC_CODE_SIZE = 6 # Size of access code to re-program device
_NDEF_DATA_SIZE = 54
# from nfcdef.h
_NDEF_URI_TYPE = ord('U')
_NDEF_TEXT_TYPE = ord('T')
# From nfcforum-ts-rtd-uri-1.0.pdf
uri_identifiers = [
(0x01, "http://www.",),
(0x02, "https://www.",),
(0x03, "http://",),
(0x04, "https://",),
(0x05, "tel:",),
(0x06, "mailto:",),
(0x07, "ftp://anonymous:anonymous@",),
(0x08, "ftp://ftp.",),
(0x09, "ftps://",),
(0x0a, "sftp://",),
(0x0b, "smb://",),
(0x0c, "nfs://",),
(0x0d, "ftp://",),
(0x0e, "dav://",),
(0x0f, "news:",),
(0x10, "telnet://",),
(0x11, "imap:",),
(0x12, "rtsp://",),
(0x13, "urn:",),
(0x14, "pop:",),
(0x15, "sip:",),
(0x16, "sips:",),
(0x17, "tftp:",),
(0x18, "btspp://",),
(0x19, "btl2cap://",),
(0x1a, "btgoep://",),
(0x1b, "tcpobex://",),
(0x1c, "irdaobex://",),
(0x1d, "file://",),
(0x1e, "urn:epc:id:",),
(0x1f, "urn:epc:tag:",),
(0x20, "urn:epc:pat:",),
(0x21, "urn:epc:raw:",),
(0x22, "urn:epc:",),
(0x23, "urn:nfc:",),
]
_NDEF_SLOTS = {
1: SLOT.NDEF,
2: SLOT.NDEF2
}
class YubiKeyNEO_USBHIDError(yubico_exception.YubicoError):
""" Exception raised for errors with the NEO USB HID communication. """
class YubiKeyNEO_USBHIDCapabilities(yubikey_usb_hid.YubiKeyUSBHIDCapabilities):
"""
Capabilities of current YubiKey NEO.
"""
def have_challenge_response(self, mode):
return self.version >= (3, 0, 0)
def have_configuration_slot(self, slot):
if self.version < (3, 0, 0):
return (slot == 1)
return slot in [1, 2]
def have_nfc_ndef(self, slot=1):
if self.version < (3, 0, 0):
return slot == 1
return slot in [1, 2]
def have_scanmap(self):
return self.version >= (3, 0, 0)
def have_device_config(self):
return self.version >= (3, 0, 0)
def have_usb_mode(self, mode):
if not self.have_device_config():
return False
mode &= ~MODE.FLAG_EJECT # Mask away eject flag
return mode in [0, 1, 2, 3, 4, 5, 6]
class YubiKeyNEO_USBHID(yubikey_usb_hid.YubiKeyUSBHID):
"""
Class for accessing a YubiKey NEO over USB HID.
The NEO is very similar to the original YubiKey (YubiKeyUSBHID)
but does add the NDEF "slot".
The NDEF is the tag the YubiKey emmits over it's NFC interface.
"""
model = 'YubiKey NEO'
description = 'YubiKey NEO'
_capabilities_cls = YubiKeyNEO_USBHIDCapabilities
def __init__(self, debug=False, skip=0, hid_device=None):
"""
Find and connect to a YubiKey NEO (USB HID).
Attributes :
skip -- number of YubiKeys to skip
debug -- True or False
"""
super(YubiKeyNEO_USBHID, self).__init__(debug, skip, hid_device)
if self.version_num() >= (2, 1, 4,) and \
self.version_num() <= (2, 1, 9,):
self.description = 'YubiKey NEO BETA'
elif self.version_num() < (3, 0, 0):
raise yubikey_base.YubiKeyVersionError("Incorrect version for %s" % self)
def write_ndef(self, ndef, slot=1):
"""
Write an NDEF tag configuration to the YubiKey NEO.
"""
if not self.capabilities.have_nfc_ndef(slot):
raise yubikey_base.YubiKeyVersionError("NDEF slot %i unsupported in %s" % (slot, self))
return self._device._write_config(ndef, _NDEF_SLOTS[slot])
def init_device_config(self, **kwargs):
return YubiKeyNEO_DEVICE_CONFIG(**kwargs)
def write_device_config(self, device_config):
"""
Write a DEVICE_CONFIG to the YubiKey NEO.
"""
if not self.capabilities.have_usb_mode(device_config._mode):
raise yubikey_base.YubiKeyVersionError("USB mode: %02x not supported for %s" % (device_config._mode, self))
return self._device._write_config(device_config, SLOT.DEVICE_CONFIG)
def write_scan_map(self, scanmap=None):
if not self.capabilities.have_scanmap():
raise yubikey_base.YubiKeyVersionError("Scanmap not supported in %s" % self)
return self._device._write_config(YubiKeyNEO_SCAN_MAP(scanmap), SLOT.SCAN_MAP)
class YubiKeyNEO_NDEF(object):
"""
Class allowing programming of a YubiKey NEO NDEF.
"""
ndef_type = _NDEF_URI_TYPE
ndef_str = None
access_code = yubico_util.chr_byte(0x0) * _ACC_CODE_SIZE
# For _NDEF_URI_TYPE
ndef_uri_rt = 0x0 # No prepending
# For _NDEF_TEXT_TYPE
ndef_text_lang = b'en'
ndef_text_enc = 'UTF-8'
def __init__(self, data, access_code = None):
self.ndef_str = data
if access_code is not None:
self.access_code = access_code
def text(self, encoding = 'UTF-8', language = 'en'):
"""
Configure parameters for NDEF type TEXT.
@param encoding: The encoding used. Should be either 'UTF-8' or 'UTF16'.
@param language: ISO/IANA language code (see RFC 3066).
"""
self.ndef_type = _NDEF_TEXT_TYPE
self.ndef_text_lang = language
self.ndef_text_enc = encoding
return self
def type(self, url = False, text = False, other = None):
"""
Change the NDEF type.
"""
if (url, text, other) == (True, False, None):
self.ndef_type = _NDEF_URI_TYPE
elif (url, text, other) == (False, True, None):
self.ndef_type = _NDEF_TEXT_TYPE
elif (url, text, type(other)) == (False, False, int):
self.ndef_type = other
else:
raise YubiKeyNEO_USBHIDError("Bad or conflicting NDEF type specified")
return self
def to_string(self):
"""
Return the current NDEF as a string (always 64 bytes).
"""
data = self.ndef_str
if self.ndef_type == _NDEF_URI_TYPE:
data = self._encode_ndef_uri_type(data)
elif self.ndef_type == _NDEF_TEXT_TYPE:
data = self._encode_ndef_text_params(data)
if len(data) > _NDEF_DATA_SIZE:
raise YubiKeyNEO_USBHIDError("NDEF payload too long")
# typedef struct {
# unsigned char len; // Payload length
# unsigned char type; // NDEF type specifier
# unsigned char data[NDEF_DATA_SIZE]; // Payload size
# unsigned char curAccCode[ACC_CODE_SIZE]; // Access code
# } YKNDEF;
#
fmt = '< B B %ss %ss' % (_NDEF_DATA_SIZE, _ACC_CODE_SIZE)
first = struct.pack(fmt,
len(data),
self.ndef_type,
data.ljust(_NDEF_DATA_SIZE, b'\0'),
self.access_code,
)
#crc = 0xffff - yubico_util.crc16(first)
#second = first + struct.pack('<H', crc) + self.unlock_code
return first
def to_frame(self, slot=SLOT.NDEF):
"""
Return the current configuration as a YubiKeyFrame object.
"""
data = self.to_string()
payload = data.ljust(64, b'\0')
return yubikey_frame.YubiKeyFrame(command = slot, payload = payload)
def _encode_ndef_uri_type(self, data):
"""
Implement NDEF URI Identifier Code.
This is a small hack to replace some well known prefixes (such as http://)
with a one byte code. If the prefix is not known, 0x00 is used.
"""
t = 0x0
for (code, prefix) in uri_identifiers:
if data[:len(prefix)].decode('latin-1').lower() == prefix:
t = code
data = data[len(prefix):]
break
data = yubico_util.chr_byte(t) + data
return data
def _encode_ndef_text_params(self, data):
"""
Prepend language and enconding information to data, according to
nfcforum-ts-rtd-text-1-0.pdf
"""
status = len(self.ndef_text_lang)
if self.ndef_text_enc == 'UTF16':
status = status & 0b10000000
return yubico_util.chr_byte(status) + self.ndef_text_lang + data
class YubiKeyNEO_DEVICE_CONFIG(object):
"""
Class allowing programming of a YubiKey NEO DEVICE_CONFIG.
"""
_mode = MODE.OTP
_cr_timeout = 0
_auto_eject_time = 0
def __init__(self, mode=MODE.OTP):
self._mode = mode
def cr_timeout(self, timeout = 0):
"""
Configure the challenge-response timeout in seconds.
"""
self._cr_timeout = timeout
return self
def auto_eject_time(self, auto_eject_time = 0):
"""
Configure the auto eject time in 10x seconds.
"""
self._auto_eject_time = auto_eject_time
return self
def to_string(self):
"""
Return the current DEVICE_CONFIG as a string (always 4 bytes).
"""
fmt = '<BBH'
first = struct.pack(
fmt,
self._mode,
self._cr_timeout,
self._auto_eject_time
)
#crc = 0xffff - yubico_util.crc16(first)
#second = first + struct.pack('<H', crc)
return first
def to_frame(self, slot=SLOT.DEVICE_CONFIG):
"""
Return the current configuration as a YubiKeyFrame object.
"""
data = self.to_string()
payload = data.ljust(64, b'\0')
return yubikey_frame.YubiKeyFrame(command=slot, payload=payload)
class YubiKeyNEO_SCAN_MAP(object):
"""
Class allowing programming of a YubiKey NEO scan map.
"""
def __init__(self, scanmap=None):
if scanmap:
if scanmap.startswith(b'h:'):
scanmap = binascii.unhexlify(scanmap[2:])
if len(scanmap) != 45:
raise yubico_exception.InputError('Scan map must be exactly 45 bytes')
self.scanmap = scanmap
def to_frame(self, slot=SLOT.SCAN_MAP):
"""
Return the current configuration as a YubiKeyFrame object.
"""
payload = self.scanmap.ljust(64, b'\0')
return yubikey_frame.YubiKeyFrame(command=slot, payload=payload)