xref: /libbtbb/python/pcaptools/btaptap (revision 48fe012ac445727d6a0bf0074d6f218d5616862f)
1#!/usr/bin/env python3
2#
3# Copyright 2009 Joshua Wright, Michael Ossmann
4#
5# This file is part of gr-bluetooth
6#
7# gr-bluetooth is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2, or (at your option)
10# any later version.
11#
12# gr-bluetooth is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with gr-bluetooth; see the file COPYING.  If not, write to
19# the Free Software Foundation, Inc., 51 Franklin Street,
20# Boston, MA 02110-1301, USA.
21
22import sys
23import struct
24import time
25from pcapdump.pcapdump import *
26
27DLT_EN10MB = 1
28DLT_BLUETOOTH_HCI_H4 = 187
29ELLISYS_CSV_HDR = "\"Depth\",\"Time\",\"Name\",\"Data\"\x0d\x0a"
30ELLISYS_HID_INPUT = "HID Input 1"
31USBHID_MAP = {
32    0x04 : "a",
33    0x05 : "b",
34    0x06 : "c",
35    0x07 : "d",
36    0x08 : "e",
37    0x09 : "f",
38    0x0A : "g",
39    0x0B : "h",
40    0x0C : "i",
41    0x0D : "j",
42    0x0E : "k",
43    0x0F : "l",
44    0x10 : "m",
45    0x11 : "n",
46    0x12 : "o",
47    0x13 : "p",
48    0x14 : "q",
49    0x15 : "r",
50    0x16 : "s",
51    0x17 : "t",
52    0x18 : "u",
53    0x19 : "v",
54    0x1A : "w",
55    0x1B : "x",
56    0x1C : "y",
57    0x1D : "z",
58    0x1E : "1",
59    0x1F : "2",
60    0x20 : "3",
61    0x21 : "4",
62    0x22 : "5",
63    0x23 : "6",
64    0x24 : "7",
65    0x25 : "8",
66    0x26 : "9",
67    0x27 : "0",
68    0x28 : "[Return]\n",
69    0x29 : "[Esc]",
70    0x2A : "[Backspace]",
71    0x2B : "[Tab]\t",
72    0x2C : " ",
73    0x2D : "-",
74    0x2E : "=",
75    0x2F : "[",
76    0x30 : "]",
77    0x31 : "\\",
78    0x32 : "#",
79    0x33 : ";",
80    0x34 : "'",
81    0x35 : "[Grave Accent]",
82    0x36 : ",",
83    0x37 : ".",
84    0x38 : "/",
85    0x39 : "[Caps Lock]",
86    0x3A : "[F1]",
87    0x3B : "[F2]",
88    0x3C : "[F3]",
89    0x3D : "[F4]",
90    0x3E : "[F5]",
91    0x3F : "[F6]",
92    0x40 : "[F7]",
93    0x41 : "[F8]",
94    0x42 : "[F9]",
95    0x43 : "[F10]",
96    0x44 : "[F11]",
97    0x45 : "[F12]",
98    0x46 : "[PrintScreen]",
99    0x47 : "[Scroll]",
100    0x48 : "[Pause]",
101    0x49 : "[Insert]",
102    0x4A : "[Home]",
103    0x4B : "[PageUp]",
104    0x4C : "[Delete]",
105    0x4D : "[End]",
106    0x4E : "[PageDown]",
107    0x4F : "[RightArrow]",
108    0x50 : "[LeftArrow]",
109    0x51 : "[DownArrow]",
110    0x52 : "[UpArrow]",
111    0x53 : "[Keypad Num Lock and Clear]",
112    0x54 : "[Keypad /]",
113    0x55 : "[Keypad *]",
114    0x56 : "[Keypad -]",
115    0x57 : "[Keypad +]",
116    0x58 : "[Keypad Enter]\n",
117    0x59 : "[Keypad 1 and End]",
118    0x5A : "[Keypad 2 and Down Arrow]",
119    0x5B : "[Keypad 3 and PageDn]",
120    0x5C : "[Keypad 4 and Left Arrow]",
121    0x5D : "[Keypad 5]",
122    0x5E : "[Keypad 6 and Right Arrow]",
123    0x5F : "[Keypad 7 and Home]",
124    0x60 : "[Keypad 8 and Up Arrow]",
125    0x61 : "[Keypad 9 and PageUp]",
126    0x62 : "[Keypad 0 and Insert]",
127    0x63 : "[Keypad . and Delete]",
128    0x64 : "\\",
129    0x65 : "[WinKey]",
130    0x66 : "[Power9]",
131    0x67 : "[Keypad =]",
132    0x68 : "[F13]",
133    0x69 : "[F14]",
134    0x6A : "[F15]",
135    0x6B : "[F16]",
136    0x6C : "[F17]",
137    0x6D : "[F18]",
138    0x6E : "[F19]",
139    0x6F : "[F20]",
140    0x70 : "[F21]",
141    0x71 : "[F22]",
142    0x72 : "[F23]",
143    0x73 : "[F24]",
144    0x74 : "[Execute]",
145    0x75 : "[Help]",
146    0x76 : "[Menu]",
147    0x77 : "[Select]",
148    0x78 : "[Stop]",
149    0x79 : "[Again]",
150    0x7A : "[Undo]",
151    0x7B : "[Cut]",
152    0x7C : "[Copy]",
153    0x7D : "[Paste]",
154    0x7E : "[Find]",
155    0x7F : "[Mute]",
156    0x80 : "[Volume Up]",
157    0x81 : "[Volume Down]",
158    0x82 : "[Locking Caps Lock]",
159    0x83 : "[Locking Num Lock]",
160    0x84 : "[Locking Scroll Lock]",
161    0x85 : "[Keypad Comma]",
162    0x86 : "[Keypad Equal]",
163    0x87 : "[International1]",
164    0x88 : "[International2]",
165    0x89 : "[International3]",
166    0x8A : "[International4]",
167    0x8B : "[International5]",
168    0x8C : "[International6]",
169    0x8D : "[International7]",
170    0x8E : "[International8]",
171    0x8F : "[International9]",
172    0x90 : "[LANG1]",
173    0x91 : "[LANG2]",
174    0x92 : "[LANG3]",
175    0x93 : "[LANG4]",
176    0x94 : "[LANG5]",
177    0x95 : "[LANG6]",
178    0x96 : "[LANG7]",
179    0x97 : "[LANG8]",
180    0x98 : "[LANG9]",
181    0x99 : "[Alternate Erase]",
182    0x9A : "[SysReq/Attention]",
183    0x9B : "[Cancel]",
184    0x9C : "[Clear]",
185    0x9D : "[Prior]",
186    0x9E : "[Return]\n",
187    0x9F : "[Separator]",
188    0xA0 : "[Out]",
189    0xA1 : "[Oper]",
190    0xA2 : "[Clear/Again]",
191    0xA3 : "[CrSel/Props]",
192    0xA4 : "[ExSel]",
193    0xB0 : "[Keypad 00]",
194    0xB1 : "[Keypad 000]",
195    0xB2 : "[Thousands Separator]",
196    0xB3 : "[Decimal Separator]",
197    0xB4 : "[Currency Unit]",
198    0xB5 : "[Currency Sub-unit]",
199    0xB6 : "[Keypad (]",
200    0xB7 : "[Keypad )]",
201    0xB8 : "[Keypad {]",
202    0xB9 : "[Keypad }]",
203    0xBA : "[Keypad Tab]\t",
204    0xBB : "[Keypad Backspace]",
205    0xBC : "[Keypad A]",
206    0xBD : "[Keypad B]",
207    0xBE : "[Keypad C]",
208    0xBF : "[Keypad D]",
209    0xC0 : "[Keypad E]",
210    0xC1 : "[Keypad F]",
211    0xC2 : "[Keypad XOR]",
212    0xC3 : "[Keypad ^]",
213    0xC4 : "[Keypad %]",
214    0xC5 : "[Keypad <]",
215    0xC6 : "[Keypad >]",
216    0xC7 : "[Keypad &]",
217    0xC8 : "[Keypad &&]",
218    0xC9 : "[Keypad |]",
219    0xCA : "[Keypad ||]",
220    0xCB : "[Keypad :]",
221    0xCC : "[Keypad #]",
222    0xCD : "[Keypad Space]",
223    0xCE : "[Keypad @]",
224    0xCF : "[Keypad !]",
225    0xD0 : "[Keypad Memory Store]",
226    0xD1 : "[Keypad Memory Recall]",
227    0xD2 : "[Keypad Memory Clear]",
228    0xD3 : "[Keypad Memory Add]",
229    0xD4 : "[Keypad Memory Subtract]",
230    0xD5 : "[Keypad Memory Multiply]",
231    0xD6 : "[Keypad Memory Divide]",
232    0xD7 : "[Keypad +/-]",
233    0xD8 : "[Keypad Clear]",
234    0xD9 : "[Keypad Clear Entry]",
235    0xDA : "[Keypad Binary]",
236    0xDB : "[Keypad Octal]",
237    0xDC : "[Keypad Decimal]",
238    0xDD : "[Keypad Hexadecimal]",
239    0xE0 : "[LeftControl]",
240    0xE1 : "[LeftShift]",
241    0xE2 : "[LeftAlt]",
242    0xE3 : "[LeftWinKey]",
243    0xE4 : "[RightControl]",
244    0xE5 : "[RightShift]",
245    0xE6 : "[RightAlt]",
246    0xE7 : "[RightWinKey]"
247}
248
249# some keycodes represent different things when Shift is held down
250USBHID_SHIFT_MAP = {
251    0x04 : "A",
252    0x05 : "B",
253    0x06 : "C",
254    0x07 : "D",
255    0x08 : "E",
256    0x09 : "F",
257    0x0A : "G",
258    0x0B : "H",
259    0x0C : "I",
260    0x0D : "J",
261    0x0E : "K",
262    0x0F : "L",
263    0x10 : "M",
264    0x11 : "N",
265    0x12 : "O",
266    0x13 : "P",
267    0x14 : "Q",
268    0x15 : "R",
269    0x16 : "S",
270    0x17 : "T",
271    0x18 : "U",
272    0x19 : "V",
273    0x1A : "W",
274    0x1B : "X",
275    0x1C : "Y",
276    0x1D : "Z",
277    0x1E : "!",
278    0x1F : "@",
279    0x20 : "#",
280    0x21 : "$",
281    0x22 : "%",
282    0x23 : "^",
283    0x24 : "&",
284    0x25 : "*",
285    0x26 : "(",
286    0x27 : ")",
287    0x2D : "_",
288    0x2E : "+",
289    0x2F : "{",
290    0x30 : "}",
291    0x31 : "|",
292    0x32 : "~",
293    0x33 : ":",
294    0x34 : "\"",
295    0x35 : "~",
296    0x36 : "<",
297    0x37 : ">",
298    0x38 : "?",
299    0x64 : "|"
300}
301
302# global variable to track currently depressed keys
303active_keys = []
304
305def hid2ascii(scancode, shift):
306    '''
307    Convert the specified scancode value to the ASCII equivalent using the
308    USBHID_MAP list.
309    '''
310    if shift:
311        try:
312            code = USBHID_SHIFT_MAP[scancode]
313            return code
314        except KeyError:
315            pass
316    try:
317        code = USBHID_MAP[scancode]
318    except KeyError:
319        return "[Reserved]"
320    return code
321
322def usage():
323    print("Usage: btaptap [-r pcapfile.pcap | -e ellisysfile.csv] [-c count] [-h]\n", file=sys.stderr)
324    sys.exit(0)
325
326def parse_l2cap_keydata(l2cappkt):
327    global active_keys
328
329    TRANS_HDR_IN_DATA = 0xA1
330    REPORT_ID_KEYBOARD = 0x01
331    CTRL = 1
332    SHIFT = 2
333    ALT = 4
334    GUI = 8
335
336    # Keyboard keystrokes are only seen in L2CAP packets at least 10 bytes long
337    l2clen = (ord(l2cappkt[1]) << 8) | ord(l2cappkt[0])
338    if l2clen < 10:
339        return
340
341    # Keyboard keystrokes are only carried by Channel ID >= 0x40
342    cid = (ord(l2cappkt[3]) << 8) | ord(l2cappkt[2])
343    if cid < 0x40:
344        return
345    # Ideally we would check for the particular CID for the HID_INTERRUPT
346    # channel, but we don't handle the negotiation (and may not have even
347    # seen it).
348
349    # Transaction Header should indicate input data
350    thdr = ord(l2cappkt[4])
351    if thdr != TRANS_HDR_IN_DATA:
352        return
353
354    # Report ID should indicate this is a keyboard
355    rid = ord(l2cappkt[5])
356    if rid != REPORT_ID_KEYBOARD:
357        return
358
359    # This byte describes modifier key status (one bit per key)
360    mod = ord(l2cappkt[6])
361
362    # We don't care whether left or right modifier keys are pressed, so we
363    # combine the status bits.
364    leftmod = mod & 0x0f
365    rightmod = (mod & 0xf0) >> 4
366    mod = leftmod | rightmod
367
368    # up to six keys can be reported at once
369    keycodes = []
370    #for byte in range(8,14):
371    for byte in range(8,11):
372        keystroke = ord(l2cappkt[byte])
373        if keystroke != 0x00:
374            keycodes.append(keystroke)
375
376    for keystroke in keycodes:
377        # don't repeat keys that are still held down
378        if active_keys.count(keystroke) == 0:
379
380            if (mod & CTRL):
381                sys.stdout.write("CTRL^")
382            if (mod & ALT):
383                sys.stdout.write("ALT^")
384            if (mod & GUI): # e.g. Windows key
385                sys.stdout.write("GUI^")
386
387            sys.stdout.write(hid2ascii(keystroke, mod & SHIFT))
388
389    active_keys = keycodes
390
391def parse_ellisys_export(exportfile):
392    try:
393        cap = open(exportfile, "r")
394    except (OSError, IOError) as e:
395        print("Unable to open Ellisys capture file.", file=sys.stderr)
396        return
397
398    # Check to make sure the CSV file header matches out expectations
399    hdr=cap.readline()
400    if (hdr != ELLISYS_CSV_HDR):
401        print("Invalid CSV file (does not match Ellisys export format)", file=sys.stderr)
402        return
403
404    for packetline in cap.xreadlines():
405        try:
406            (edepth, etime, ename, edata) = packetline.replace('"', '').strip().split(",")
407        except ValueError:
408            continue
409
410        # We are only interedted in HID Input data
411        if (ename != ELLISYS_HID_INPUT): continue
412
413        parse_ellisys_keydata(edata)
414
415def parse_ellisys_keydata(payload):
416    TRANS_HDR_IN_DATA = "\xA1"
417    DEST_CHANNEL_ID = "\x06\x03"
418
419    # Convert space-separated hex into string
420    payload = payload.replace(' ','').decode("hex")
421
422    # The Ellisys CSV file format doesn't give us the L2CAP data, so we fake it here by adding
423    # a 2-byte length field, destination CID, and transaction header
424    packet = chr(len(payload)+1) + "\x00" + DEST_CHANNEL_ID + TRANS_HDR_IN_DATA + payload
425    parse_l2cap_keydata(packet)
426
427def parse_bb_keydata(packet):
428
429    BTBBHDR_TYPE_MASK = 0x78
430    BTBBHDR_TYPE_SHIFT = 3
431    BTBBHDR_TYPE_DM1 = 3
432    BTBBPAYLOADHDR_LLID_MASK = 0x03
433    BTBBPAYLOADHDR_LLID_SHIFT = 0
434    BTBBPAYLOADHDR_LEN_MASK = 0xF8
435    BTBBPAYLOADHDR_LEN_SHIFT = 3
436    LLID_L2CAP = 2
437
438    # Keyboard keystrokes are only seen in frames at least 40 bytes long
439    if len(packet) < 40:
440        return
441
442    # Keyboard keystrokes are only seen in DM1 frames
443    btbbhdr = packet[20:23]
444    type = (ord(btbbhdr[0]) & BTBBHDR_TYPE_MASK) >> BTBBHDR_TYPE_SHIFT
445    if type != BTBBHDR_TYPE_DM1:
446        return
447
448    # Keyboard keystrokes are only seen in L2CAP packets 14 bytes long
449    btbbpayloadhdr = ord(packet[23])
450    llid = btbbpayloadhdr & (BTBBPAYLOADHDR_LLID_MASK) >> BTBBPAYLOADHDR_LLID_SHIFT
451    l2clen = (btbbpayloadhdr & BTBBPAYLOADHDR_LEN_MASK) >> BTBBPAYLOADHDR_LEN_SHIFT
452    #print("Debug btbbpayloadhdr 0x%02x, llid %d, l2clen %d"%(btbbpayloadhdr, llid, l2clen))
453    if llid != LLID_L2CAP or l2clen < 14:
454        return
455
456    parse_l2cap_keydata(packet[24:38])
457
458def parse_hci_keydata(packet):
459
460    HCI_TYPE_ACL_DATA = 2
461
462    # Keyboard keystrokes are only seen in frames at least 19 bytes long
463    if len(packet) < 19:
464        return
465
466    # Keyboard keystrokes are only seen in ACL Data frames
467    type = ord(packet[0])
468    if type != HCI_TYPE_ACL_DATA:
469        return
470
471    parse_l2cap_keydata(packet[5:])
472
473if __name__ == '__main__':
474
475    arg_pcapfile = None
476    arg_ellisysfile = None
477    arg_count = -1
478    packetcount = 0
479
480    while len(sys.argv) > 1:
481        op = sys.argv.pop(1)
482        if op == '-r':
483            arg_pcapfile = sys.argv.pop(1)
484        if op == '-c':
485            arg_count = int(sys.argv.pop(1))
486        if op == '-e':
487            arg_ellisysfile = sys.argv.pop(1)
488        if op == '-h':
489            usage()
490            sys.exit(0)
491
492    if (arg_ellisysfile == None and arg_pcapfile == None):
493        print("Must specify a libpcap capture or an Ellisys CSV file", file=sys.stderr)
494        usage()
495        sys.exit(0)
496
497    if arg_pcapfile != None:
498        cap = PcapReader(arg_pcapfile)
499
500        while arg_count != packetcount:
501            try:
502                (pheader, packet) = cap.pnext()
503                pkttime = pheader[0]
504                packetcount+=1
505
506                if cap.datalink() == DLT_EN10MB:
507                    parse_bb_keydata(packet)
508                elif cap.datalink() == DLT_BLUETOOTH_HCI_H4:
509                    parse_hci_keydata(packet)
510                else:
511                    print("Unsupported libpcap data link layer: %d\n" % cap.datalink(), file=sys.stderr)
512            except TypeError: # raised when pnext returns Null (end of capture)
513                break
514
515        cap.close()
516
517    if arg_ellisysfile != None:
518        parse_ellisys_export(arg_ellisysfile)
519
520    print
521