# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""HID Transport for U2F.

This module imports the U2F HID Transport protocol as well as methods
for discovering devices implementing this protocol.
"""
from __future__ import absolute_import

import logging
import os
import struct
import time
import six

from . import hid


def HidUsageSelector(device):
  if device['usage_page'] == 0xf1d0 and device['usage'] == 0x01:
    return True
  return False


def DiscoverLocalHIDU2FDevices(selector=HidUsageSelector):
  for d in hid.Enumerate():
    if selector(d):
      try:
        dev = hid.Open(d['path'])
        yield UsbHidTransport(dev)
      except OSError:
        # Insufficient permissions to access device
        pass


class UsbHidTransport(object):
  """Implements the U2FHID transport protocol.

  This class implements the U2FHID transport protocol from the
  FIDO U2F specs.  This protocol manages fragmenting longer messages
  over a short hid frame (usually 64 bytes).  It exposes an APDU
  channel through the MSG command as well as a series of other commands
  for configuring and interacting with the device.
  """

  U2FHID_PING = 0x81
  U2FHID_MSG = 0x83
  U2FHID_WINK = 0x88
  U2FHID_PROMPT = 0x87
  U2FHID_INIT = 0x86
  U2FHID_LOCK = 0x84
  U2FHID_ERROR = 0xbf
  U2FHID_SYNC = 0xbc

  CTAPHID_KEEPALIVE = 0xbb

  U2FHID_BROADCAST_CID = bytearray([0xff, 0xff, 0xff, 0xff])

  ERR_CHANNEL_BUSY = bytearray([0x06])


  class InitPacket(object):
    """Represent an initial U2FHID packet.

    Represent an initial U2FHID packet.  This packet contains
    metadata necessary to interpret the entire packet stream associated
    with a particular exchange (read or write).

    Attributes:
      packet_size: The size of the hid report (packet) used.  Usually 64.
      cid: The channel id for the connection to the device.
      size: The size of the entire message to be sent (including
          all continuation packets)
      payload: The portion of the message to put into the init packet.
          This must be smaller than packet_size - 7 (the overhead for
          an init packet).
    """

    def __init__(self, packet_size, cid, cmd, size, payload):
      self.packet_size = packet_size
      if len(cid) != 4 or cmd > 255 or size >= 2**16:
        raise OSError('Invalid packet')
      if len(payload) > self.packet_size - 7:
        raise OSError('Invalid packet')

      self.cid = cid  # byte array
      self.cmd = cmd  # number
      self.size = size  # number (full size of message)
      self.payload = payload  # byte array (for first packet)

    def ToWireFormat(self):
      """Serializes the packet."""
      ret = bytearray(64)
      ret[0:4] = self.cid
      ret[4] = self.cmd
      struct.pack_into('>H', ret, 5, self.size)
      ret[7:7 + len(self.payload)] = self.payload
      return list(six.iterbytes(bytes(ret)))

    @staticmethod
    def FromWireFormat(packet_size, data):
      """Derializes the packet.

      Deserializes the packet from wire format.

      Args:
        packet_size: The size of all packets (usually 64)
        data: List of ints or bytearray containing the data from the wire.

      Returns:
        InitPacket object for specified data

      Raises:
        OSError: if the data isn't a valid InitPacket
      """
      ba = bytearray(data)
      if len(ba) != packet_size:
        raise OSError('Invalid packet')
      cid = ba[0:4]
      cmd = ba[4]
      size = struct.unpack('>H', bytes(ba[5:7]))[0]
      payload = ba[7:7 + size]  # might truncate at packet_size
      return UsbHidTransport.InitPacket(packet_size, cid, cmd, size, payload)

  class ContPacket(object):
    """Represents a continutation U2FHID packet.

    Represents a continutation U2FHID packet.  These packets follow
    the intial packet and contains the remaining data in a particular
    message.

    Attributes:
      packet_size: The size of the hid report (packet) used.  Usually 64.
      cid: The channel id for the connection to the device.
      seq: The sequence number for this continuation packet.  The first
          continuation packet is 0 and it increases from there.
      payload:  The payload to put into this continuation packet.  This
          must be less than packet_size - 5 (the overhead of the
          continuation packet is 5).
    """

    def __init__(self, packet_size, cid, seq, payload):
      self.packet_size = packet_size
      self.cid = cid
      self.seq = seq
      self.payload = payload
      if len(payload) > self.packet_size - 5:
        raise OSError('Invalid packet')
      if seq > 127:
        raise OSError('Invalid packet')

    def ToWireFormat(self):
      """Serializes the packet."""
      ret = bytearray(self.packet_size)
      ret[0:4] = self.cid
      ret[4] = self.seq
      ret[5:5 + len(self.payload)] = self.payload
      return [int(x) for x in ret]

    @staticmethod
    def FromWireFormat(packet_size, data):
      """Derializes the packet.

      Deserializes the packet from wire format.

      Args:
        packet_size: The size of all packets (usually 64)
        data: List of ints or bytearray containing the data from the wire.

      Returns:
        InitPacket object for specified data

      Raises:
        OSError: if the data isn't a valid ContPacket
      """
      ba = bytearray(data)
      if len(ba) != packet_size:
        raise OSError('Invalid packet')
      cid = ba[0:4]
      seq = ba[4]
      # We don't know the size limit a priori here without seeing the init
      # packet, so truncation needs to be done in the higher level protocol
      # handling code, unlike the degenerate case of a 1 packet message in an
      # init packet, where the size is known.
      payload = ba[5:]
      return UsbHidTransport.ContPacket(packet_size, cid, seq, payload)

  def __init__(self, hid_device, read_timeout_secs=3.0):
    self.hid_device = hid_device

    in_size = hid_device.GetInReportDataLength()
    out_size = hid_device.GetOutReportDataLength()
    if in_size != out_size:
      raise OSError(
          'unsupported device with different in/out packet sizes.')
    if in_size == 0:
      raise OSError('unable to determine packet size')

    self.packet_size = in_size
    self.read_timeout_secs = read_timeout_secs
    self.logger = logging.getLogger('_pyu2f.hidtransport')

    self.InternalInit()

  def SendMsgBytes(self, msg):
    r = self.InternalExchange(UsbHidTransport.U2FHID_MSG, msg)
    return r

  def SendBlink(self, length):
    return self.InternalExchange(UsbHidTransport.U2FHID_PROMPT,
                                 bytearray([length]))

  def SendWink(self):
    return self.InternalExchange(UsbHidTransport.U2FHID_WINK, bytearray([]))

  def SendPing(self, data):
    return self.InternalExchange(UsbHidTransport.U2FHID_PING, data)

  def InternalInit(self):
    """Initializes the device and obtains channel id."""
    self.cid = UsbHidTransport.U2FHID_BROADCAST_CID
    nonce = bytearray(os.urandom(8))
    r = self.InternalExchange(UsbHidTransport.U2FHID_INIT, nonce)
    if len(r) < 17:
      raise OSError('unexpected init reply len')
    if r[0:8] != nonce:
      raise OSError('nonce mismatch')
    self.cid = bytearray(r[8:12])

    self.u2fhid_version = r[12]
    self.device_version = tuple(r[13:16])
    self.capabilities = r[16]

  def InternalExchange(self, cmd, payload_in):
    """Sends and receives a message from the device."""
    # make a copy because we destroy it below
    self.logger.debug('payload: ' + str(list(payload_in)))
    payload = bytearray()
    payload[:] = payload_in
    for _ in range(2):
      self.InternalSend(cmd, payload)
      ret_cmd, ret_payload = self.InternalRecv()

      if ret_cmd == UsbHidTransport.U2FHID_ERROR:
        if ret_payload == UsbHidTransport.ERR_CHANNEL_BUSY:
          time.sleep(0.5)
          continue
        raise OSError('Device error: %d' % int(ret_payload[0]))
      elif ret_cmd != cmd:
        raise OSError('Command mismatch!')

      return ret_payload
    raise OSError('Device Busy.  Please retry')

  def InternalSend(self, cmd, payload):
    """Sends a message to the device, including fragmenting it."""
    length_to_send = len(payload)

    max_payload = self.packet_size - 7
    first_frame = payload[0:max_payload]
    first_packet = UsbHidTransport.InitPacket(self.packet_size, self.cid, cmd,
                                              len(payload), first_frame)
    del payload[0:max_payload]
    length_to_send -= len(first_frame)
    self.InternalSendPacket(first_packet)

    seq = 0
    while length_to_send > 0:
      max_payload = self.packet_size - 5
      next_frame = payload[0:max_payload]
      del payload[0:max_payload]
      length_to_send -= len(next_frame)
      next_packet = UsbHidTransport.ContPacket(self.packet_size, self.cid, seq,
                                               next_frame)
      self.InternalSendPacket(next_packet)
      seq += 1

  def InternalSendPacket(self, packet):
    wire = packet.ToWireFormat()
    self.logger.debug('sending packet: ' + str(wire))
    self.hid_device.Write(wire)

  def InternalReadFrame(self):
        # TODO(gdasher): Figure out timeouts.  Today, this implementation
        # blocks forever at the HID level waiting for a response to a report.
        # This may not be reasonable behavior (though in practice in seems to be
        # OK on the set of devices and machines tested so far).
    frame = self.hid_device.Read()
    self.logger.debug('recv: ' + str(frame))
    return frame

  def InternalRecv(self):
    """Receives a message from the device, including defragmenting it."""
    first_packet_read = False
    while not first_packet_read:
        first_read = self.InternalReadFrame()
        first_packet = UsbHidTransport.InitPacket.FromWireFormat(
            self.packet_size, first_read)
        first_packet_read = self.cid == first_packet.cid

    data = first_packet.payload
    to_read = first_packet.size - len(first_packet.payload)

    seq = 0
    while to_read > 0:
      next_read = self.InternalReadFrame()
      next_packet = UsbHidTransport.ContPacket.FromWireFormat(self.packet_size,
                                                              next_read)
      if self.cid != next_packet.cid:
        # Skip over packets that are for communication with other clients.
        # HID is broadcast, so we see potentially all communication from the
        # device.  For well-behaved devices, these should be BUSY messages
        # sent to other clients of the device because at this point we're
        # in mid-message transit.
        continue

      if seq != next_packet.seq:
        raise OSError('Packets received out of order')

      # This packet for us at this point, so debit it against our
      # balance of bytes to read.
      to_read -= len(next_packet.payload)

      data.extend(next_packet.payload)
      seq += 1

    # truncate incomplete frames
    data = data[0:first_packet.size]
    return (first_packet.cmd, data)
