# Copyright (c) 2020 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.
import fcntl
import glob
import logging
import struct
import sys
from typing import Set
from yubikit.core.otp import OtpConnection
from yubikit.logging import LOG_LEVEL
from .base import USAGE_OTP, YUBICO_VID, OtpYubiKeyDevice
# Don't typecheck this file on Windows
assert sys.platform != "win32" # nosec
logger = logging.getLogger(__name__)
# usb_ioctl.h
USB_GET_REPORT = 0xC0094807
USB_SET_REPORT = 0xC0094806
# hidraw.h
HIDIOCGRAWINFO = 0x80084803
HIDIOCGRDESCSIZE = 0x80044801
HIDIOCGRDESC = 0x90044802
[docs]
class HidrawConnection(OtpConnection):
def __init__(self, path):
self.handle = open(path, "wb")
[docs]
def close(self):
self.handle.close()
[docs]
def receive(self):
buf = bytearray(1 + 8)
fcntl.ioctl(self.handle, USB_GET_REPORT, buf, True)
data = buf[1:]
logger.log(LOG_LEVEL.TRAFFIC, "RECV: %s", data.hex())
return data
[docs]
def send(self, data):
logger.log(LOG_LEVEL.TRAFFIC, "SEND: %s", data.hex())
buf = bytearray([0]) # Prepend the report ID.
buf.extend(data)
fcntl.ioctl(self.handle, USB_SET_REPORT, buf, True)
[docs]
def get_info(dev):
buf = bytearray(4 + 2 + 2)
fcntl.ioctl(dev, HIDIOCGRAWINFO, buf, True)
return struct.unpack("<IHH", buf)
[docs]
def get_descriptor(dev):
buf = bytearray(4)
fcntl.ioctl(dev, HIDIOCGRDESCSIZE, buf, True)
size = struct.unpack("<I", buf)[0]
buf += bytearray(size)
fcntl.ioctl(dev, HIDIOCGRDESC, buf, True)
return buf[4:]
[docs]
def get_usage(dev):
buf = get_descriptor(dev)
usage, usage_page = (None, None)
while buf:
head, buf = buf[0], buf[1:]
typ, size = 0xFC & head, 0x03 & head
value, buf = buf[:size], buf[size:]
if typ == 4: # Usage page
usage_page = struct.unpack("<I", value.ljust(4, b"\0"))[0]
if usage is not None:
return usage_page, usage
elif typ == 8: # Usage
usage = struct.unpack("<I", value.ljust(4, b"\0"))[0]
if usage_page is not None:
return usage_page, usage
# Cache for continuously failing devices
_failed_cache: Set[str] = set()
[docs]
def list_devices():
devices = []
for hidraw in glob.glob("/dev/hidraw*"):
try:
with open(hidraw, "rb") as f:
bustype, vid, pid = get_info(f)
if vid == YUBICO_VID and get_usage(f) == USAGE_OTP:
devices.append(OtpYubiKeyDevice(hidraw, pid, HidrawConnection))
if hidraw in _failed_cache:
_failed_cache.remove(hidraw)
except Exception:
if hidraw not in _failed_cache:
logger.debug(
f"Couldn't read HID descriptor for {hidraw}", exc_info=True
)
_failed_cache.add(hidraw)
continue
return devices