device.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. """XAP Device
  2. """
  3. import hid
  4. import json
  5. import random
  6. import gzip
  7. import threading
  8. import functools
  9. from struct import Struct, pack, unpack
  10. from collections import namedtuple
  11. from platform import platform
  12. from .types import XAPSecureStatus, XAPFlags, XAPRouteError
  13. RequestPacket = namedtuple('RequestPacket', 'token length data')
  14. RequestStruct = Struct('<HB61s')
  15. ResponsePacket = namedtuple('ResponsePacket', 'token flags length data')
  16. ResponseStruct = Struct('<HBB60s')
  17. def _gen_token():
  18. """Generate XAP token - cannot start with 00xx or 'reserved' (FFFE|FFFF)
  19. """
  20. token = random.randrange(0x0100, 0xFFFD)
  21. # swap endianness
  22. return unpack('<H', pack('>H', token))[0]
  23. def _u32toBCD(val): # noqa: N802
  24. """Create BCD string
  25. """
  26. return f'{val>>24}.{val>>16 & 0xFF}.{val & 0xFFFF}'
  27. class XAPDevice:
  28. def __init__(self, dev):
  29. """Constructor opens hid device and starts dependent services
  30. """
  31. self.responses = {}
  32. self.dev = hid.Device(path=dev['path'])
  33. self.bg = threading.Thread(target=self._read_loop, daemon=True)
  34. self.bg.start()
  35. def _read_loop(self):
  36. """Background thread to signal waiting transactions
  37. """
  38. while 1:
  39. array_alpha = self.dev.read(ResponseStruct.size, 100)
  40. if array_alpha:
  41. token = int.from_bytes(array_alpha[:2], 'little')
  42. event = self.responses.get(token)
  43. if event:
  44. event._ret = array_alpha
  45. event.set()
  46. def _query_device_info(self):
  47. datalen = int.from_bytes(self.transaction(b'\x01\x05') or bytes(0), 'little')
  48. if not datalen:
  49. return {}
  50. data = []
  51. offset = 0
  52. while offset < datalen:
  53. chunk = self.transaction(b'\x01\x06', offset)
  54. data += chunk
  55. offset += len(chunk)
  56. str_data = gzip.decompress(bytearray(data[:datalen]))
  57. return json.loads(str_data)
  58. def listen(self):
  59. """Receive a 'broadcast' message
  60. """
  61. token = 0xFFFF
  62. event = threading.Event()
  63. self.responses[token] = event
  64. while not hasattr(event, '_ret'):
  65. event.wait(timeout=0.25)
  66. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  67. return (r.flags, r.data[:r.length])
  68. def _transaction(self, *args):
  69. """Request/Receive
  70. """
  71. # convert args to array of bytes
  72. data = bytes()
  73. for arg in args:
  74. if isinstance(arg, (bytes, bytearray)):
  75. data += arg
  76. if isinstance(arg, int): # TODO: remove terrible assumption of u16
  77. data += arg.to_bytes(2, byteorder='little')
  78. token = _gen_token()
  79. p = RequestPacket(token, len(data), data)
  80. buffer = RequestStruct.pack(*list(p))
  81. event = threading.Event()
  82. self.responses[token] = event
  83. # prepend 0 on windows because reasons...
  84. if 'windows' in platform().lower():
  85. buffer = b'\x00' + buffer
  86. self.dev.write(buffer)
  87. event.wait(timeout=1)
  88. self.responses.pop(token, None)
  89. if not hasattr(event, '_ret'):
  90. return None
  91. r = ResponsePacket._make(ResponseStruct.unpack(event._ret))
  92. if r.flags & XAPFlags.SUCCESS == 0:
  93. return None
  94. return r.data[:r.length]
  95. @functools.lru_cache
  96. def capability(self, route):
  97. cap = int.from_bytes(self._transaction(route) or bytes(0), 'little')
  98. return cap
  99. @functools.lru_cache
  100. def subsystem(self):
  101. sub = int.from_bytes(self._transaction(b'\x00\x02') or bytes(0), 'little')
  102. return sub
  103. @functools.lru_cache
  104. def version(self):
  105. ver = int.from_bytes(self._transaction(b'\x00\x00') or bytes(0), 'little')
  106. return {'xap': _u32toBCD(ver)}
  107. def _ensure_route(self, route):
  108. (sub, rt) = route
  109. cap = bytes([sub, 1])
  110. if self.subsystem() & (1 << sub) == 0:
  111. raise XAPRouteError("subsystem not available")
  112. if self.capability(cap) & (1 << rt) == 0:
  113. raise XAPRouteError("route not available")
  114. def transaction(self, route, *args):
  115. self._ensure_route(route)
  116. return self._transaction(route, *args)
  117. @functools.lru_cache
  118. def info(self):
  119. data = self._query_device_info()
  120. data['_id'] = self.transaction(b'\x01\x08')
  121. data['xap'] = self.version()['xap']
  122. return data
  123. def status(self):
  124. lock = int.from_bytes(self.transaction(b'\x00\x03') or bytes(0), 'little')
  125. data = {}
  126. data['lock'] = XAPSecureStatus(lock).name
  127. return data
  128. def unlock(self):
  129. self.transaction(b'\x00\x04')
  130. def lock(self):
  131. self.transaction(b'\x00\x05')
  132. def reset(self):
  133. status = int.from_bytes(self.transaction(b'\x01\x07') or bytes(0), 'little')
  134. return status == 1