1
0

xap_client.py 6.0 KB

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