1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
|
#!/usr/bin/env python3
import time
import enum
import sys
from contextlib import contextmanager, suppress, wraps
import serial
from cobs import cobs
import uinput
from noise.connection import NoiseConnection, Keypair
from noise.exceptions import NoiseInvalidMessage
import keymap
from hexdump import hexdump
class PacketType(enum.Enum):
_RESERVED = 0
INITIATE_HANDSHAKE = 1
HANDSHAKE = 2
DATA = 3
COMM_ERROR = 4
CRYPTO_ERROR = 5
TOO_MANY_FAILS = 6
class ReportType(enum.Enum):
_RESERVED = 0
KEYBOARD = 1
MOUSE = 2
PAIRING_INPUT = 3
PAIRING_SUCESS = 4
PAIRING_ERROR = 5
class ProtocolError(Exception):
pass
class Packetizer:
def __init__(self, serial, debug=False, width=16):
self.ser, self.debug, self.width = serial, debug, width
self.ser.write(b'\0') # COBS synchronization
def send_packet(self, pkt_type, data):
if self.debug:
print(f'\033[93mSending {len(data)} bytes, packet type {pkt_type.name} ({pkt_type.value})\033[0m')
hexdump(print, data, self.width)
data = bytes([pkt_type.value]) + data
encoded = cobs.encode(data) + b'\0'
self.ser.write(encoded)
self.ser.flushOutput()
def receive_packet(self):
packet = self.ser.read_until(b'\0')
data = cobs.decode(packet[:-1])
if self.debug:
print(f'\033[93mReceived {len(data)} bytes\033[0m')
hexdump(print, data, self.width)
pkt_type, data = PacketType(data[0]), data[1:]
if pkt_type is PacketType.COMM_ERROR:
raise ProtocolError('Device-side serial communication error')
elif pkt_type is PacketType.CRYPTO_ERROR:
raise ProtocolError('Device-side cryptographic error')
elif pkt_type is PacketType.TOO_MANY_FAILS:
raise ProtocolError('Device reports too many failed handshake attempts')
else:
return pkt_type, data
class KeyMapper:
Keycode = enum.Enum('Keycode', start=0, names='''
KEY_NONE _RESERVED_0x01 _RESERVED_0x02 _RESERVED_0x03 KEY_A KEY_B KEY_C KEY_D
KEY_E KEY_F KEY_G KEY_H KEY_I KEY_J KEY_K KEY_L
KEY_M KEY_N KEY_O KEY_P KEY_Q KEY_R KEY_S KEY_T
KEY_U KEY_V KEY_W KEY_X KEY_Y KEY_Z KEY_1 KEY_2
KEY_3 KEY_4 KEY_5 KEY_6 KEY_7 KEY_8 KEY_9 KEY_0
KEY_ENTER KEY_ESC KEY_BACKSPACE KEY_TAB KEY_SPACE KEY_MINUS KEY_EQUAL KEY_LEFTBRACE
KEY_RIGHTBRACE KEY_BACKSLASH KEY_HASH KEY_SEMICOLON KEY_APOSTROPHE KEY_GRAVE KEY_COMMA KEY_DOT
KEY_SLASH KEY_CAPSLOCK KEY_F1 KEY_F2 KEY_F3 KEY_F4 KEY_F5 KEY_F6
KEY_F7 KEY_F8 KEY_F9 KEY_F10 KEY_F11 KEY_F12 KEY_SYSRQ KEY_SCROLLLOCK
KEY_PAUSE KEY_INSERT KEY_HOME KEY_PAGEUP KEY_DELETE KEY_END KEY_PAGEDOWN KEY_RIGHT
KEY_LEFT KEY_DOWN KEY_UP KEY_NUMLOCK KEY_KPSLASH KEY_KPASTERISK KEY_KPMINUS KEY_KPPLUS
KEY_KPENTER KEY_KP1 KEY_KP2 KEY_KP3 KEY_KP4 KEY_KP5 KEY_KP6 KEY_KP7
KEY_KP8 KEY_KP9 KEY_KP0 KEY_KPDOT KEY_102ND KEY_COMPOSE KEY_POWER KEY_KPEQUAL
KEY_F13 KEY_F14 KEY_F15 KEY_F16 KEY_F17 KEY_F18 KEY_F19 KEY_F20
KEY_F21 KEY_F22 KEY_F23 KEY_F24 KEY_OPEN KEY_HELP KEY_PROPS KEY_FRONT
KEY_STOP KEY_AGAIN KEY_UNDO KEY_CUT KEY_COPY KEY_PASTE KEY_FIND KEY_MUTE
KEY_VOLUMEUP KEY_VOLUMEDOWN _RESERVED_0x82 _RESERVED_0x83 _RESERVED_0x84 KEY_KPCOMMA _RESERVED_0x86 KEY_RO
KEY_KATAKANAHIRAGANA KEY_YEN KEY_HENKAN KEY_MUHENKAN KEY_KPJPCOMMA _RESERVED_0x8D _RESERVED_0x8E _RESERVED_0x8F
KEY_HANGEUL KEY_HANJA KEY_KATAKANA KEY_HIRAGANA KEY_ZENKAKUHANKAKU _RESERVED_0x95 _RESERVED_0x96 _RESERVED_0x97
_RESERVED_0x98 _RESERVED_0x99 _RESERVED_0x9A _RESERVED_0x9B _RESERVED_0x9C _RESERVED_0x9D _RESERVED_0x9E _RESERVED_0x9F
_RESERVED_0xA0 _RESERVED_0xA1 _RESERVED_0xA2 _RESERVED_0xA3 _RESERVED_0xA4 _RESERVED_0xA5 _RESERVED_0xA6 _RESERVED_0xA7
_RESERVED_0xA8 _RESERVED_0xA9 _RESERVED_0xAA _RESERVED_0xAB _RESERVED_0xAC _RESERVED_0xAD _RESERVED_0xAE _RESERVED_0xAF
_RESERVED_0xB0 _RESERVED_0xB1 _RESERVED_0xB2 _RESERVED_0xB3 _RESERVED_0xB4 _RESERVED_0xB5 KEY_KPLEFTPAREN KEY_KPRIGHTPAREN
_RESERVED_0xB8 _RESERVED_0xB9 _RESERVED_0xBA _RESERVED_0xBB _RESERVED_0xBC _RESERVED_0xBD _RESERVED_0xBE _RESERVED_0xBF
_RESERVED_0xC0 _RESERVED_0xC1 _RESERVED_0xC2 _RESERVED_0xC3 _RESERVED_0xC4 _RESERVED_0xC5 _RESERVED_0xC6 _RESERVED_0xC7
_RESERVED_0xC8 _RESERVED_0xC9 _RESERVED_0xCA _RESERVED_0xCB _RESERVED_0xCC _RESERVED_0xCD _RESERVED_0xCE _RESERVED_0xCF
_RESERVED_0xD0 _RESERVED_0xD1 _RESERVED_0xD2 _RESERVED_0xD3 _RESERVED_0xD4 _RESERVED_0xD5 _RESERVED_0xD6 _RESERVED_0xD7
_RESERVED_0xD8 _RESERVED_0xD9 _RESERVED_0xDA _RESERVED_0xDB _RESERVED_0xDC _RESERVED_0xDD _RESERVED_0xDE _RESERVED_0xDF
_RESERVED_0xE0 _RESERVED_0xE1 _RESERVED_0xE2 _RESERVED_0xE3 _RESERVED_0xE4 _RESERVED_0xE5 _RESERVED_0xE6 _RESERVED_0xE7
_RESERVED_0xE8 _RESERVED_0xE9 _RESERVED_0xEA _RESERVED_0xEB _RESERVED_0xEC _RESERVED_0xED _RESERVED_0xEE _RESERVED_0xEF
_RESERVED_0xF0 _RESERVED_0xF1 _RESERVED_0xF2 _RESERVED_0xF3 _RESERVED_0xF4 _RESERVED_0xF5 _RESERVED_0xF6 _RESERVED_0xF7
_RESERVED_0xF8 _RESERVED_0xF9 _RESERVED_0xFA _RESERVED_0xFB _RESERVED_0xFC _RESERVED_0xFD _RESERVED_0xFE _RESERVED_0xFF
''')
MODIFIERS = [ uinput.ev.KEY_LEFTCTRL, uinput.ev.KEY_LEFTSHIFT, uinput.ev.KEY_LEFTALT, uinput.ev.KEY_LEFTMETA,
uinput.ev.KEY_RIGHTCTRL, uinput.ev.KEY_RIGHTSHIFT, uinput.ev.KEY_RIGHTALT, uinput.ev.KEY_RIGHTMETA ]
ALL_KEYS = [ v for k, v in uinput.ev.__dict__.items() if k.startswith('KEY_') ]
REGULAR_MAP = { kc.value: getattr(uinput.ev, kc.name) for kc in Keycode if hasattr(uinput.ev, kc.name) }
@classmethod
def map_modifiers(kls, val):
return [ mod for i, mod in enumerate(kls.MODIFIERS) if val & (1<<i) ]
@classmethod
def map_regulars(kls, keycodes):
return [ kls.REGULAR_MAP[kc] for kc in keycodes if kc != 0 and kc in kls.REGULAR_MAP ]
class Magic:
@classmethod
def map_bytes_to_incantation(kls, data):
elems = [ f'{kls.ADJECTIVES[a]} {kls.NOUNS[b]}' for a, b in zip(data[0::2], data[1::2]) ]
nfirst = ", ".join(elems[:-1])
return f'{nfirst} and {elems[-1]}'
ADJECTIVES = '''
wrathful worthy weird warm volatile veiled vacuous useless
upset unsoiled unsightly unpronounceable unfriendly unfree unfit unfaithful
unchaste unbroken unbound unblessed unbefitting unaltered unabused unable
ugly tongued thorny thirsty thick terminal ten-sided teeming
tangerine taken substantial stupefying stringy strange stillborn sticky
stagnant spongy sour soul-destroying smoldering smitten slain six-sided
shifting shadowy severed seven-sided serene salty rust-red royal
rotten riddled resentful regrettable reeking rare rank rancid
quiescent putrid putrid putrescent prehistoric predatory predaceous porous
poisonous pierced phlegmatic petrifying pessimal pathetic odorless oddish
obsessed obscene numb nine-sided nasty mysterious mute musky
morose moribund moldy miasmic material many-lobed malodorous malign
maimed luminescent low-cut lousy live limp lifeless leering
leaky layered latent lackluster jagged irregular iridescent intangible
infinite inept incomprehensible in-between improper idle hunted hideous
heavy hairy guilty grotesque grey greedy gory gorgeous
gooey golden-brown golden ghastly frostbitten fresh-cut freakish frantic
fossilized formless formidable floccose five-lobed firstborn filthy fickle
fetid fertile fearful fatal familiar fallen fallacious faint
faceless extinct esoteric errant emergent elastic eight-sided eerie
ebon dysphoric dying dumb dull-purple dull dull dull
dormant doomed disfigured dirty defenseless deep-pink deep deconsecrated
deathlike deadly dead dark-blue dark curly curious cured
cunning crystalline cryptic crying crumbly crimson crested creepy
crazy corrupt corporeal contemptible contained concrete cloudy chopped
chained caustic catholic cathartic captive cancerous cabalistic burnt
buoyant bronze-red bronze broken bright-red breathless bound bound
bottomless bony bodiless blue-lilac blue bloody bloodthirsty bloodsucking
bloodstained bloodcurdling blonde blistered blank bitter bilgy bewitched
befouled beardless bastardly barbed baleful balding awkward awful
atrocious arcane appalling antic anonymous angry ample ambiguous
amber-green amber aghast activated acidic abused abstruse abject
'''.split()
NOUNS = '''
yolk writing wrath wound worm wings whistle watchdog
waste vomit vermin variation underachievement tusk troll trick
transplant transgression tooth tongue tickle tick thorn thistle
thing terror tentacle tease surrender surge sucker substance
storm stone stew stalk squid sprout sponge spill
spider sphere spectacle speck spawn soul solution snout
snake smell sloth slime slice sleeper slave sinew
shell shape seizure seed schism scam scale sainthood
root robe roach rinse remains relay rejuvenation realization
reaction ransom pupa pride prey predator potion pornography
polyp plum pleasure pitch pigeon phenomenon pest periwinkle
percolation parasite pair oyster orphan orgasm organism orchid
object nail mushroom murder mucus movement mother mold
mist mildew metal mesh meddling mayhem masterpiece masonry
mask manhood maggot lust loop living_thing liquor liquid
lining laceration knife kitten kiss jumper jest instrument
injustice injury influence indulgence incursion impulse imago hound
horn hook hoof heirloom heart hawk hare hair
gulp guardian grass goat gnat gluttony glowworm gasp
game fusion fungus frustration frog foul foot food
fog foal fluke fluff flower flicker flea flattery
flask flare firefly finger filtration female feeder feather
fart fang failure face fabrication extract exodus evil
envy enema embryo egress echo eater ear dwarf
dust drop draft domestication distortion dew depravity deity
death daughter dash dagger culture crutch crow critter
creeper creation crab corruption cocoon claw chip child
cell catch carving carrot carnival cancer butterfly burn
buildup brush brew bottle boot book bone blunder
blot blood blink bite bird benthos beak basket
bark ball baby axolotl ashes artifact arson armor
apparition antenna alms alienation advent adornment abomination abandonment
'''.split()
class NoiseEngine:
def __init__(self, packetizer, debug=False):
self.debug = debug
self.packetizer = packetizer
self.static_local = bytes([ # FIXME
0xbb, 0xdb, 0x4c, 0xdb, 0xd3, 0x09, 0xf1, 0xa1, 0xf2, 0xe1, 0x45, 0x69, 0x67, 0xfe, 0x28, 0x8c,
0xad, 0xd6, 0xf7, 0x12, 0xd6, 0x5d, 0xc7, 0xb7, 0x79, 0x3d, 0x5e, 0x63, 0xda, 0x6b, 0x37, 0x5b
])
self.proto = NoiseConnection.from_name(b'Noise_XX_25519_ChaChaPoly_BLAKE2s')
self.proto.set_as_initiator()
self.proto.set_keypair_from_private_bytes(Keypair.STATIC, self.static_local)
self.proto.start_handshake()
self.packetizer.send_packet(PacketType.INITIATE_HANDSHAKE, b'')
self.debug_print('Handshake started')
@wraps(print)
def debug_print(self, *args, **kwargs):
if self.debug:
print(*args, **kwargs)
def perform_handshake(self):
while True:
if self.proto.handshake_finished:
break
self.packetizer.send_packet(PacketType.HANDSHAKE, self.proto.write_message())
if self.proto.handshake_finished:
break
pkt_type, payload = self.packetizer.receive_packet()
if pkt_type is PacketType.HANDSHAKE:
self.proto.read_message(payload)
else:
raise ProtocolError(f'Incorrect packet type {pkt_type}. Ignoring since this is only test code.')
if self.debug:
print('Handshake finished, handshake hash:')
hexdump(print, self.proto.get_handshake_hash())
def channel_binding_incantation(self):
hhash = self.proto.get_handshake_hash()
return '\n'.join(Magic.map_bytes_to_incantation(hhash[i:i+8]) for i in range(0, 16, 8))
def receive_loop(self):
while True:
try:
pkt_type, received = self.packetizer.receive_packet()
except Exception as e:
self.debug_print('Invalid framing:', e)
if pkt_type is not PacketType.DATA:
raise UserWarning(f'Unexpected packet type {pkt_type}. Ignoring.')
continue
rtype, data = self._decrypt(received)
if self.debug:
print(f'Decrypted packet {rtype} ({rtype.value}):')
hexdump(print, data)
yield rtype, data
def _decrypt(self, received):
try:
data = self.proto.decrypt(received)
return ReportType(data[0]), data[1:]
except NoiseInvalidMessage as e:
self.debug_print('Invalid noise message', e)
for i in range(3):
with self._nonce_lookahead() as set_nonce:
set_nonce(i)
data = self.proto.decrypt(received)
return ReportType(data[0]), data[1:]
else:
self.debug_print(' Unrecoverable.')
raise e
self.debug_print(f' Recovered. n={n}')
@contextmanager
def _nonce_lookahead(self):
nold = self.proto.noise_protocol.cipher_state_decrypt.n
def setter(n):
self.proto.noise_protocol.cipher_state_decrypt.n = nold + n
with suppress(NoiseInvalidMessage):
yield setter
self.proto.noise_protocol.cipher_state_decrypt.n = nold
def pairing_messages(self):
user_input = ''
for msg_type, payload in self.receive_loop():
if msg_type == ReportType.PAIRING_INPUT:
ch = chr(payload[0])
if ch == '\b':
user_input = user_input[:-1]
else:
user_input += ch
yield user_input
elif msg_type == ReportType.PAIRING_SUCESS:
break
elif msg_type == ReportType.PAIRING_ERROR:
raise ProtocolError('Device-side pairing error') # FIXME find better exception subclass here
else:
raise ProtocolError('Invalid report type')
def uinput_passthrough(self):
with uinput.Device(KeyMapper.ALL_KEYS) as ui:
old_kcs = set()
for msg_type, payload in self.receive_loop():
report_len, *report = payload
if report_len != 8:
raise ValueError('Unsupported report length', report_len)
if msg_type is ReportType.KEYBOARD:
modbyte, _reserved, *keycodes = report
import binascii
keys = { *KeyMapper.map_modifiers(modbyte), *KeyMapper.map_regulars(keycodes) }
if self.debug:
print('Emitting:', keys)
print('payload:', binascii.hexlify(payload), 'emitting:', keys)
for key in keys - old_kcs:
ui.emit(key, 1, syn=False)
for key in old_kcs - keys:
ui.emit(key, 0, syn=False)
ui.syn()
old_kcs = keys
elif msg_type is ReportType.MOUSE:
# FIXME unhandled
pass
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('serial')
parser.add_argument('baudrate')
parser.add_argument('-w', '--width', type=int, default=16, help='Number of bytes to display in one line')
parser.add_argument('-d', '--debug', action='store_true')
args = parser.parse_args()
ser = serial.Serial(args.serial, args.baudrate)
packetizer = Packetizer(ser, debug=args.debug, width=args.width)
noise = NoiseEngine(packetizer, debug=args.debug)
noise.perform_handshake()
print('Handshake channel binding incantation:')
print(noise.channel_binding_incantation())
for user_input in noise.pairing_messages():
if not args.debug:
print('\033[2K\r', end='')
print('Pairing input:', user_input, end='' if not args.debug else '\n', flush=True)
print()
print('Pairing success')
noise.uinput_passthrough()
|