xap_client.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Dummy XAP Client
  2. """
  3. import json
  4. import random
  5. import gzip
  6. import threading
  7. import functools
  8. from struct import Struct, pack, unpack
  9. from collections import namedtuple
  10. from enum import IntFlag, IntEnum
  11. from platform import platform
  12. RequestPacket = namedtuple('RequestPacket', 'token length data')
  13. RequestStruct = Struct('<HB61s')
  14. ResponsePacket = namedtuple('ResponsePacket', 'token flags length data')
  15. ResponseStruct = Struct('<HBB60s')
  16. def _gen_token():
  17. """Generate XAP token - cannot start with 00xx or 'reserved' (FFFE|FFFF)
  18. """
  19. token = random.randrange(0x0100, 0xFFFD)
  20. # swap endianness
  21. return unpack('<H', pack('>H', token))[0]
  22. def _u32toBCD(val): # noqa: N802
  23. """Create BCD string
  24. """
  25. return f'{val>>24}.{val>>16 & 0xFF}.{val & 0xFFFF}'
  26. class XAPSecureStatus(IntEnum):
  27. LOCKED = 0x00
  28. UNLOCKING = 0x01
  29. UNLOCKED = 0x02
  30. class XAPFlags(IntFlag):
  31. FAILURE = 0
  32. SUCCESS = 1 << 0
  33. SECURE_FAILURE = 1 << 1
  34. class XAPEventType(IntEnum):
  35. SECURE = 0x01
  36. KEYBOARD = 0x02
  37. USER = 0x03
  38. class XAPRouteError(Exception):
  39. pass
  40. class XAPDevice:
  41. def __init__(self, dev):
  42. """Constructor opens hid device and starts dependent services
  43. """
  44. self.responses = {}
  45. self.dev = hid.Device(path=dev['path'])
  46. self.bg = threading.Thread(target=self._read_loop, daemon=True)
  47. self.bg.start()
  48. def _read_loop(self):
  49. """Background thread to signal waiting transactions
  50. """
  51. while 1:
  52. array_alpha = self.dev.read(ResponseStruct.size, 100)
  53. if array_alpha:
  54. token = int.from_bytes(array_alpha[:2], 'little')
  55. event = self.responses.get(token)
  56. if event:
  57. event._ret = array_alpha
  58. event.set()
  59. def _query_device_info(self):
  60. datalen = int.from_bytes(self.transaction(b'\x01\x05') or bytes(0), 'little')
  61. if not datalen:
  62. return {}
  63. data = []
  64. offset = 0
  65. while offset < datalen:
  66. chunk = self.transaction(b'\x01\x06', offset)
  67. data += chunk
  68. offset += len(chunk)
  69. str_data = gzip.decompress(bytearray(data[:datalen]))
  70. return json.loads(str_data)
  71. def listen(self):
  72. """Receive a 'broadcast' message
  73. """
  74. token = 0xFFFF
  75. event = threading.Event()
  76. self.responses[token] = event
  77. while not hasattr(event, '_ret'):
  78. event.wait(timeout=0.25)
  79. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  80. return (r.flags, r.data[:r.length])
  81. def _transaction(self, *args):
  82. """Request/Receive
  83. """
  84. # convert args to array of bytes
  85. data = bytes()
  86. for arg in args:
  87. if isinstance(arg, (bytes, bytearray)):
  88. data += arg
  89. if isinstance(arg, int): # TODO: remove terrible assumption of u16
  90. data += arg.to_bytes(2, byteorder='little')
  91. token = _gen_token()
  92. p = RequestPacket(token, len(data), data)
  93. buffer = RequestStruct.pack(*list(p))
  94. event = threading.Event()
  95. self.responses[token] = event
  96. # prepend 0 on windows because reasons...
  97. if 'windows' in platform().lower():
  98. buffer = b'\x00' + buffer
  99. self.dev.write(buffer)
  100. event.wait(timeout=1)
  101. self.responses.pop(token, None)
  102. if not hasattr(event, '_ret'):
  103. return None
  104. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  105. if r.flags & XAPFlags.SUCCESS == 0:
  106. return None
  107. return r.data[:r.length]
  108. @functools.lru_cache
  109. def capability(self, route):
  110. cap = int.from_bytes(self._transaction(route) or bytes(0), 'little')
  111. return cap
  112. @functools.lru_cache
  113. def subsystem(self):
  114. sub = int.from_bytes(self._transaction(b'\x00\x02') or bytes(0), 'little')
  115. return sub
  116. @functools.lru_cache
  117. def version(self):
  118. ver = int.from_bytes(self._transaction(b'\x00\x00') or bytes(0), 'little')
  119. return {'xap': _u32toBCD(ver)}
  120. def _ensure_route(self, route):
  121. (sub, rt) = route
  122. cap = bytes([sub, 1])
  123. if self.subsystem() & (1 << sub) == 0:
  124. raise XAPRouteError("subsystem not available")
  125. if self.capability(cap) & (1 << rt) == 0:
  126. raise XAPRouteError("route not available")
  127. def transaction(self, route, *args):
  128. self._ensure_route(route)
  129. return self._transaction(route, *args)
  130. @functools.lru_cache
  131. def info(self):
  132. data = self._query_device_info()
  133. data['_id'] = self.transaction(b'\x01\x08')
  134. data['xap'] = self.version()['xap']
  135. return data
  136. def status(self):
  137. lock = int.from_bytes(self.transaction(b'\x00\x03') or bytes(0), 'little')
  138. data = {}
  139. data['lock'] = XAPSecureStatus(lock).name
  140. return data
  141. def unlock(self):
  142. self.transaction(b'\x00\x04')
  143. def lock(self):
  144. self.transaction(b'\x00\x05')
  145. def reset(self):
  146. status = int.from_bytes(self.transaction(b'\x01\x07') or bytes(0), 'little')
  147. return status == 1
  148. class XAPClient:
  149. @staticmethod
  150. def _lazy_imports():
  151. # Lazy load to avoid missing dependency issues
  152. global hid
  153. import hid
  154. @staticmethod
  155. def list(search=None):
  156. """Find compatible XAP devices
  157. """
  158. XAPClient._lazy_imports()
  159. def _is_xap_usage(x):
  160. return x['usage_page'] == 0xFF51 and x['usage'] == 0x0058
  161. def _is_filtered_device(x):
  162. name = '%04x:%04x' % (x['vendor_id'], x['product_id'])
  163. return name.lower().startswith(search.lower())
  164. devices = filter(_is_xap_usage, hid.enumerate())
  165. if search:
  166. devices = filter(_is_filtered_device, devices)
  167. return list(devices)
  168. def connect(self, dev):
  169. """Connect to a given XAP device
  170. """
  171. XAPClient._lazy_imports()
  172. return XAPDevice(dev)