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