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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
|
#!/usr/bin/env python3
import time
import enum
import sys
from contextlib import contextmanager, suppress, wraps
import hashlib
import secrets
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_SUCCESS = 4
PAIRING_ERROR = 5
PAIRING_START = 6
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]}'
EVEN = '''
aardvark absurd accrue acme adrift adult afflict ahead
aimless Algol allow alone ammo ancient apple artist
assume Athens atlas Aztec baboon backfield backward banjo
beaming bedlamp beehive beeswax befriend Belfast berserk billiard
bison blackjack blockade blowtorch bluebird bombast bookshelf brackish
breadline breakup brickyard briefcase Burbank button buzzard cement
chairlift chatter checkup chisel choking chopper Christmas clamshell
classic classroom cleanup clockwork cobra commence concert cowbell
crackdown cranky crowfoot crucial crumpled crusade cubic dashboard
deadbolt deckhand dogsled dragnet drainage dreadful drifter dropper
drumbeat drunken Dupont dwelling eating edict egghead eightball
endorse endow enlist erase escape exceed eyeglass eyetooth
facial fallout flagpole flatfoot flytrap fracture framework freedom
frighten gazelle Geiger glitter glucose goggles goldfish gremlin
guidance hamlet highchair hockey indoors indulge inverse involve
island jawbone keyboard kickoff kiwi klaxon locale lockup
merit minnow miser Mohawk mural music necklace Neptune
newborn nightbird Oakland obtuse offload optic orca payday
peachy pheasant physique playhouse Pluto preclude prefer preshrunk
printer prowler pupil puppy python quadrant quiver quota
ragtime ratchet rebirth reform regain reindeer rematch repay
retouch revenge reward rhythm ribcage ringbolt robust rocker
ruffled sailboat sawdust scallion scenic scorecard Scotland seabird
select sentence shadow shamrock showgirl skullcap skydive slingshot
slowdown snapline snapshot snowcap snowslide solo southward soybean
spaniel spearhead spellbind spheroid spigot spindle spyglass stagehand
stagnate stairway standard stapler steamship sterling stockman stopwatch
stormy sugar surmount suspense sweatband swelter tactics talon
tapeworm tempest tiger tissue tonic topmost tracker transit
trauma treadmill Trojan trouble tumor tunnel tycoon uncut
unearth unwind uproot upset upshot vapor village virus
Vulcan waffle wallet watchword wayside willow woodlark Zulu
'''.split()
ODD = '''
adroitness adviser aftermath aggregate alkali almighty amulet amusement
antenna applicant Apollo armistice article asteroid Atlantic atmosphere
autopsy Babylon backwater barbecue belowground bifocals bodyguard bookseller
borderline bottomless Bradbury bravado Brazilian breakaway Burlington businessman
butterfat Camelot candidate cannonball Capricorn caravan caretaker celebrate
cellulose certify chambermaid Cherokee Chicago clergyman coherence combustion
commando company component concurrent confidence conformist congregate consensus
consulting corporate corrosion councilman crossover crucifix cumbersome customer
Dakota decadence December decimal designing detector detergent determine
dictator dinosaur direction disable disbelief disruptive distortion document
embezzle enchanting enrollment enterprise equation equipment escapade Eskimo
everyday examine existence exodus fascinate filament finicky forever
fortitude frequency gadgetry Galveston getaway glossary gossamer graduate
gravity guitarist hamburger Hamilton handiwork hazardous headwaters hemisphere
hesitate hideaway holiness hurricane hydraulic impartial impetus inception
indigo inertia infancy inferno informant insincere insurgent integrate
intention inventive Istanbul Jamaica Jupiter leprosy letterhead liberty
maritime matchmaker maverick Medusa megaton microscope microwave midsummer
millionaire miracle misnomer molasses molecule Montana monument mosquito
narrative nebula newsletter Norwegian October Ohio onlooker opulent
Orlando outfielder Pacific pandemic Pandora paperweight paragon paragraph
paramount passenger pedigree Pegasus penetrate perceptive performance pharmacy
phonetic photograph pioneer pocketful politeness positive potato processor
provincial proximate puberty publisher pyramid quantity racketeer rebellion
recipe recover repellent replica reproduce resistor responsive retraction
retrieval retrospect revenue revival revolver sandalwood sardonic Saturday
savagery scavenger sensation sociable souvenir specialist speculate stethoscope
stupendous supportive surrender suspicious sympathy tambourine telephone therapist
tobacco tolerance tomorrow torpedo tradition travesty trombonist truncated
typewriter ultimate undaunted underfoot unicorn unify universe unravel
upcoming vacancy vagabond vertigo Virginia visitor vocalist voyager
warranty Waterloo whimsical Wichita Wilmington Wyoming yesteryear Yucatan
'''.split()
class NoiseEngine:
def __init__(self, host_key, packetizer, debug=False):
self.debug = debug
self.packetizer = packetizer
self.static_local = host_key
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.handshake = self.proto.noise_protocol.handshake_state # save for later because someone didn't think
self.paired = False
self.connected = False
@property
def remote_fingerprint(self):
''' Return the SHA-256 hash of the remote static key (rs). This can be used to fingerprint the remote party. '''
return hashlib.sha256(self.handshake.rs.public_bytes).hexdigest()
@classmethod
def generate_private_key_x25519(kls):
# This is taken from noise-c's reference implementation. This would not be needed had not cryptography/hazmat
# decided noone would ever need serialized x25519 private keys and noiseprotocol stopped just short of implementing
# key generation (who'd need that anyway, amiright?) -.-
key = list(secrets.token_bytes(32))
key[0] &= 0xF8
key[31] = (key[31] & 0x7F) | 0x40
return bytes(key)
@wraps(print)
def debug_print(self, *args, **kwargs):
if self.debug:
print(*args, **kwargs)
def perform_handshake(self):
self.packetizer.send_packet(PacketType.INITIATE_HANDSHAKE, b'')
self.debug_print('Handshake started')
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.')
msg_type, payload = self.packetizer.receive_packet()
rtype, data = self._decrypt(payload)
if rtype is ReportType.PAIRING_SUCCESS:
self.connected, self.paired = True, True
elif rtype is ReportType.PAIRING_START:
self.connected, self.paired = True, False
else:
self.connected, self.paired = True, False
raise UserWarning(f'Unexpected record type {rtype} in {msg_type} packet. Ignoring.')
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 is 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 is ReportType.PAIRING_SUCCESS:
self.paired = True
break
elif msg_type is 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)
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()
|