parent
bf6f88182a
commit
8b133897dc
@ -0,0 +1,186 @@ |
||||
"""Dummy XAP Client |
||||
""" |
||||
import json |
||||
import random |
||||
import gzip |
||||
import threading |
||||
import functools |
||||
from struct import Struct, pack, unpack |
||||
from collections import namedtuple |
||||
from enum import IntFlag, IntEnum |
||||
from platform import platform |
||||
|
||||
RequestPacket = namedtuple('RequestPacket', 'token length data') |
||||
RequestStruct = Struct('<HB61s') |
||||
|
||||
ResponsePacket = namedtuple('ResponsePacket', 'token flags length data') |
||||
ResponseStruct = Struct('<HBB60s') |
||||
|
||||
|
||||
def _gen_token(): |
||||
"""Generate XAP token - cannot start with 00xx or be FFFF |
||||
""" |
||||
token = random.randrange(0x0100, 0xFFFE) |
||||
|
||||
# swap endianness |
||||
return unpack('<H', pack('>H', token))[0] |
||||
|
||||
|
||||
def _u32toBCD(val): # noqa: N802 |
||||
"""Create BCD string |
||||
""" |
||||
return f'{val>>24}.{val>>16 & 0xFF}.{val & 0xFFFF}' |
||||
|
||||
|
||||
class XAPSecureStatus(IntEnum): |
||||
LOCKED = 0x00 |
||||
UNLOCKING = 0x01 |
||||
UNLOCKED = 0x02 |
||||
|
||||
|
||||
class XAPFlags(IntFlag): |
||||
FAILURE = 0 |
||||
SUCCESS = 1 << 0 |
||||
SECURE_FAILURE = 1 << 1 |
||||
UNLOCK_IN_PROGRESS = 1 << 6 |
||||
UNLOCKED = 1 << 7 |
||||
|
||||
|
||||
class XAPEventType(IntEnum): |
||||
SECURE = 0x01 |
||||
KEYBOARD = 0x02 |
||||
USER = 0x03 |
||||
|
||||
|
||||
class XAPDevice: |
||||
def __init__(self, dev): |
||||
"""Constructor opens hid device and starts dependent services |
||||
""" |
||||
self.responses = {} |
||||
|
||||
self.dev = hid.Device(path=dev['path']) |
||||
|
||||
self.bg = threading.Thread(target=self._read_loop, daemon=True) |
||||
self.bg.start() |
||||
|
||||
def _read_loop(self): |
||||
"""Background thread to signal waiting transactions |
||||
""" |
||||
while 1: |
||||
array_alpha = self.dev.read(64, 100) |
||||
if array_alpha: |
||||
token = int.from_bytes(array_alpha[:2], 'little') |
||||
event = self.responses.get(token) |
||||
if event: |
||||
event._ret = array_alpha |
||||
event.set() |
||||
|
||||
def _query_device_info(self): |
||||
datalen = int.from_bytes(self.transaction(b'\x01\x05') or bytes(0), 'little') |
||||
if not datalen: |
||||
return {} |
||||
|
||||
data = [] |
||||
offset = 0 |
||||
while offset < datalen: |
||||
chunk = self.transaction(b'\x01\x06', offset) |
||||
data += chunk |
||||
offset += len(chunk) |
||||
str_data = gzip.decompress(bytearray(data[:datalen])) |
||||
return json.loads(str_data) |
||||
|
||||
def listen(self): |
||||
"""Receive a 'broadcast' message |
||||
""" |
||||
token = 0xFFFF |
||||
event = threading.Event() |
||||
self.responses[token] = event |
||||
|
||||
event.wait() |
||||
|
||||
r = ResponsePacket._make(ResponseStruct.unpack(event._ret)) |
||||
return (r.flags, r.data[:r.length]) |
||||
|
||||
def transaction(self, *args): |
||||
"""Request/Receive |
||||
""" |
||||
# convert args to array of bytes |
||||
data = bytes() |
||||
for arg in args: |
||||
if isinstance(arg, (bytes, bytearray)): |
||||
data += arg |
||||
if isinstance(arg, int): # TODO: remove terrible assumption of u16 |
||||
data += arg.to_bytes(2, byteorder='little') |
||||
|
||||
token = _gen_token() |
||||
|
||||
p = RequestPacket(token, len(data), data) |
||||
buffer = RequestStruct.pack(*list(p)) |
||||
|
||||
event = threading.Event() |
||||
self.responses[token] = event |
||||
|
||||
# prepend 0 on windows because reasons... |
||||
if 'windows' in platform().lower(): |
||||
buffer = b'\x00' + buffer |
||||
self.dev.write(buffer) |
||||
|
||||
event.wait(timeout=1) |
||||
self.responses.pop(token, None) |
||||
if not hasattr(event, '_ret'): |
||||
return None |
||||
|
||||
r = ResponsePacket._make(ResponseStruct.unpack(event._ret)) |
||||
if r.flags != XAPFlags.SUCCESS: |
||||
return None |
||||
|
||||
return r.data[:r.length] |
||||
|
||||
@functools.cache |
||||
def version(self): |
||||
ver = int.from_bytes(self.transaction(b'\x00\x00') or bytes(0), 'little') |
||||
return {'xap': _u32toBCD(ver)} |
||||
|
||||
@functools.cache |
||||
def info(self): |
||||
data = self._query_device_info() |
||||
data['_id'] = self.transaction(b'\x01\x08') |
||||
data['xap'] = self.version()['xap'] |
||||
return data |
||||
|
||||
def unlock(self): |
||||
self.transaction(b'\x00\x04') |
||||
|
||||
|
||||
class XAPClient: |
||||
@staticmethod |
||||
def _lazy_imports(): |
||||
# Lazy load to avoid missing dependency issues |
||||
global hid |
||||
import hid |
||||
|
||||
@staticmethod |
||||
def list(search=None): |
||||
"""Find compatible XAP devices |
||||
""" |
||||
XAPClient._lazy_imports() |
||||
|
||||
def _is_xap_usage(x): |
||||
return x['usage_page'] == 0xFF51 and x['usage'] == 0x0058 |
||||
|
||||
def _is_filtered_device(x): |
||||
name = '%04x:%04x' % (x['vendor_id'], x['product_id']) |
||||
return name.lower().startswith(search.lower()) |
||||
|
||||
devices = filter(_is_xap_usage, hid.enumerate()) |
||||
if search: |
||||
devices = filter(_is_filtered_device, devices) |
||||
|
||||
return list(devices) |
||||
|
||||
def connect(self, dev): |
||||
"""Connect to a given XAP device |
||||
""" |
||||
XAPClient._lazy_imports() |
||||
|
||||
return XAPDevice(dev) |
Loading…
Reference in new issue