text: # Copyright (c) 2015 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 __future__ import absolute_import
  
  from .util import PID, TRANSPORT, Mode
  from .device import YubiKey
  from .driver_ccid import open_devices as open_ccid
  from .driver_fido import open_devices as open_fido
  from .driver_otp import open_devices as open_otp
  from .native.pyusb import get_usb_backend
  
  import logging
  import usb.core
  import time
  
  logger = logging.getLogger(__name__)
  
  
  class FailedOpeningDeviceException(Exception):
      pass
  
  
  class Descriptor(object):
  
      def __init__(self, pid, version, fingerprint, serial=None):
          self._logger = logger.getChild('Descriptor')
          self._version = version
          self._pid = pid
          self._key_type = pid.get_type()
          self._mode = Mode.from_pid(pid)
          self._fingerprint = fingerprint
  
      @property
      def fingerprint(self):
          return self._fingerprint
  
      @property
      def version(self):
          return self._version
  
      @property
      def pid(self):
          return self._pid
  
      @property
      def mode(self):
          return self._mode
  
      @property
      def key_type(self):
          return self._key_type
  
      def open_device(self, transports=sum(TRANSPORT), serial=None, attempts=10):
          self._logger.debug('transports: 0x%x, self.mode.transports: 0x%x',
                             transports, self.mode.transports)
  
          transports &= self.mode.transports
  
          logger.debug('Opening driver for serial: %s, pid: %s', serial, self.pid)
          for attempt in range(1, attempts + 1):
              logger.debug('Attempt %d of %d', attempt, attempts)
              sleep_time = attempt * 0.1
              for drv in _list_drivers(transports):
                  logger.debug('Found driver: %s, pid: %s', drv, drv.pid)
                  dev = YubiKey(self, drv)
                  if serial is not None and dev.serial != serial:
                      logger.debug('Serial does not match. Want: %s, got: %s',
                                   serial, dev.serial)
                      del dev
                      continue
                  if drv.pid != self.pid:
                      logger.debug('PID does not match. Want: %s, got: %s',
                                   self.pid, drv.pid)
                      del dev
                      continue
                  return dev
              #  Wait a little before trying again.
              logger.debug('Sleeping for %f s', sleep_time)
              time.sleep(sleep_time)
          logger.debug('No matching device found')
          raise FailedOpeningDeviceException()
  
      @classmethod
      def from_usb(cls, usb_dev):
          v_int = usb_dev.bcdDevice
          version = ((v_int >> 8) % 16, (v_int >> 4) % 16, v_int % 16)
          pid = PID(usb_dev.idProduct)
          fp = (pid, version, usb_dev.bus, usb_dev.address, usb_dev.iSerialNumber)
          return cls(pid, version, fp)
  
      @classmethod
      def from_driver(cls, driver):
          fp = (driver.pid,)
          return cls(driver.pid, None, fp)
  
  
  def _gen_descriptors():
      found = []  # Composite devices are listed multiple times on Windows...
      for dev in usb.core.find(True, idVendor=0x1050, backend=get_usb_backend()):
          try:
              addr = (dev.bus, dev.address)
              if addr not in found:
                  found.append(addr)
                  yield Descriptor.from_usb(dev)
          except ValueError as e:
              logger.debug('Invalid PID', exc_info=e)
  
  
  def get_descriptors():
      return list(_gen_descriptors())
  
  
  def _list_drivers(transports):
      if TRANSPORT.CCID & transports:
          for dev in open_ccid():
              if dev:
                  yield dev
      if TRANSPORT.OTP & transports:
          for dev in open_otp():
              if dev:
                  yield dev
      if TRANSPORT.FIDO & transports:
          for dev in open_fido():
              if dev:
                  yield dev
  
  
  def list_devices(transports=sum(TRANSPORT)):
      for d in _list_drivers(transports):
          yield YubiKey(Descriptor.from_driver(d), d)
  
  
  def _open_driver(transports, serial, pid, attempts):
      logger.debug('Opening driver for transports: %s, serial: %s, pid: %s',
                   transports, serial, pid)
      for attempt in range(1, attempts + 1):
          logger.debug('Attempt %d of %d', attempt, attempts)
          sleep_time = attempt * 0.1
          for dev in list_devices(transports):
              logger.debug('Found driver: %s serial: %s, pid: %s',
                           dev.driver, dev.serial, dev.driver.pid)
              if serial is not None and dev.serial != serial:
                  logger.debug('Serial does not match. Want: %s, got: %s',
                               serial, dev.serial)
                  del dev
                  continue
              if pid is not None and dev.driver.pid != pid:
                  logger.debug('PID does not match. Want: %s, got: %s',
                               pid, dev.driver.pid)
                  del dev
                  continue
              return dev.driver
          #  Wait a little before trying again.
          logger.debug('Sleeping for %f s', sleep_time)
          time.sleep(sleep_time)
      logger.debug('No driver found for serial: %s, pid: %s', serial, pid)
      raise FailedOpeningDeviceException()
  
  
  def open_device(transports=sum(TRANSPORT), serial=None, pid=None, attempts=10):
      driver = _open_driver(transports, serial, pid, attempts)
      matches = [d for d in get_descriptors() if d.pid == driver.pid]
      if len(matches) == 1:  # Only one matching descriptor, must be it
          descriptor = matches[0]
      else:
          descriptor = Descriptor.from_driver(driver)
      return YubiKey(descriptor, driver)
  