xap_client.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 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(64, 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. event.wait()
  78. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  79. return (r.flags, r.data[:r.length])
  80. def transaction(self, *args):
  81. """Request/Receive
  82. """
  83. # convert args to array of bytes
  84. data = bytes()
  85. for arg in args:
  86. if isinstance(arg, (bytes, bytearray)):
  87. data += arg
  88. if isinstance(arg, int): # TODO: remove terrible assumption of u16
  89. data += arg.to_bytes(2, byteorder='little')
  90. token = _gen_token()
  91. p = RequestPacket(token, len(data), data)
  92. buffer = RequestStruct.pack(*list(p))
  93. event = threading.Event()
  94. self.responses[token] = event
  95. # prepend 0 on windows because reasons...
  96. if 'windows' in platform().lower():
  97. buffer = b'\x00' + buffer
  98. self.dev.write(buffer)
  99. event.wait(timeout=1)
  100. self.responses.pop(token, None)
  101. if not hasattr(event, '_ret'):
  102. return None
  103. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  104. if r.flags != XAPFlags.SUCCESS:
  105. return None
  106. return r.data[:r.length]
  107. @functools.cache
  108. def version(self):
  109. ver = int.from_bytes(self.transaction(b'\x00\x00') or bytes(0), 'little')
  110. return {'xap': _u32toBCD(ver)}
  111. @functools.cache
  112. def info(self):
  113. data = self._query_device_info()
  114. data['_id'] = self.transaction(b'\x01\x08')
  115. data['xap'] = self.version()['xap']
  116. return data
  117. def status(self):
  118. lock = int.from_bytes(self.transaction(b'\x00\x03') or bytes(0), 'little')
  119. data = {}
  120. data['lock'] = XAPSecureStatus(lock).name
  121. return data
  122. def unlock(self):
  123. self.transaction(b'\x00\x04')
  124. def lock(self):
  125. self.transaction(b'\x00\x05')
  126. def reset(self):
  127. status = int.from_bytes(self.transaction(b'\x01\x07') or bytes(0), 'little')
  128. return status == 1
  129. class XAPClient:
  130. @staticmethod
  131. def _lazy_imports():
  132. # Lazy load to avoid missing dependency issues
  133. global hid
  134. import hid
  135. @staticmethod
  136. def list(search=None):
  137. """Find compatible XAP devices
  138. """
  139. XAPClient._lazy_imports()
  140. def _is_xap_usage(x):
  141. return x['usage_page'] == 0xFF51 and x['usage'] == 0x0058
  142. def _is_filtered_device(x):
  143. name = '%04x:%04x' % (x['vendor_id'], x['product_id'])
  144. return name.lower().startswith(search.lower())
  145. devices = filter(_is_xap_usage, hid.enumerate())
  146. if search:
  147. devices = filter(_is_filtered_device, devices)
  148. return list(devices)
  149. def connect(self, dev):
  150. """Connect to a given XAP device
  151. """
  152. XAPClient._lazy_imports()
  153. return XAPDevice(dev)