xref: /btstack/test/mesh/dump_mesh_pklg.py (revision a1146ab8547ef4c4bbd39da18b8733eaa5ee027e)
1#!/usr/bin/env python3
2# BlueKitchen GmbH (c) 2019
3
4# primitive dump for PacketLogger format
5
6# APPLE PacketLogger
7# typedef struct {
8#   uint32_t    len;
9#   uint32_t    ts_sec;
10#   uint32_t    ts_usec;
11#   uint8_t     type;   // 0xfc for note
12# }
13
14#define BLUETOOTH_DATA_TYPE_PB_ADV                                             0x29 // PB-ADV
15#define BLUETOOTH_DATA_TYPE_MESH_MESSAGE                                       0x2A // Mesh Message
16#define BLUETOOTH_DATA_TYPE_MESH_BEACON                                        0x2B // Mesh Beacon
17
18import re
19import sys
20import time
21import datetime
22import struct
23from mesh_crypto import *
24
25# state
26netkeys = {}
27appkeys = {}
28devkey = b''
29ivi = b'\x00'
30segmented_messages = {}
31
32access_messages = {
33# Foundation
340x0000: 'Config Appkey Add',
350x0001: 'Config Appkey Update',
360x0002: 'Config Composition Data Status',
370x0003: 'Config Model Publication Set',
380x0004: 'Config Health Current Status',
390x0005: 'Config Health Fault Status',
400x0006: 'Config Heartbeat Publication Status',
410x8000: 'Config Appkey Delete',
420x8001: 'Config Appkey Get',
430x8002: 'Config Appkey List',
440x8003: 'Config Appkey Status',
450x8004: 'Config Health Attention Get',
460x8005: 'Config Health Attention Set',
470x8006: 'Config Health Attention Set Unacknowledged',
480x8007: 'Config Health Attention Status',
490x8008: 'Config Composition Data Get',
500x8009: 'Config Beacon Get',
510x800a: 'Config Beacon Set',
520x800b: 'Config Beacon Status',
530x800c: 'Config Default Ttl Get',
540x800d: 'Config Default Ttl Set',
550x800e: 'Config Default Ttl Status',
560x800f: 'Config Friend Get',
570x8010: 'Config Friend Set',
580x8011: 'Config Friend Status',
590x8012: 'Config Gatt Proxy Get',
600x8013: 'Config Gatt Proxy Set',
610x8014: 'Config Gatt Proxy Status',
620x8015: 'Config Key Refresh Phase Get',
630x8016: 'Config Key Refresh Phase Set',
640x8017: 'Config Key Refresh Phase Status',
650x8018: 'Config Model Publication Get',
660x8019: 'Config Model Publication Status',
670x801a: 'Config Model Publication Virtual Address Set',
680x801b: 'Config Model Subscription Add',
690x801c: 'Config Model Subscription Delete',
700x801d: 'Config Model Subscription Delete All',
710x801e: 'Config Model Subscription Overwrite',
720x801f: 'Config Model Subscription Status',
730x8020: 'Config Model Subscription Virtual Address Add',
740x8021: 'Config Model Subscription Virtual Address Delete',
750x8022: 'Config Model Subscription Virtual Address Overwrite',
760x8023: 'Config Network Transmit Get',
770x8024: 'Config Network Transmit Set',
780x8025: 'Config Network Transmit Status',
790x8026: 'Config Relay Get',
800x8027: 'Config Relay Set',
810x8028: 'Config Relay Status',
820x8029: 'Config Sig Model Subscription Get',
830x802a: 'Config Sig Model Subscription List',
840x802b: 'Config Vendor Model Subscription Get',
850x802c: 'Config Vendor Model Subscription List',
860x802d: 'Config Low Power Node Poll Timeout Get',
870x802e: 'Config Low Power Node Poll Timeout Status',
880x802f: 'Config Health Fault Clear',
890x8030: 'Config Health Fault Clear Unacknowledged',
900x8031: 'Config Health Fault Get',
910x8032: 'Config Health Fault Test',
920x8033: 'Config Health Fault Test Unacknowledged',
930x8034: 'Config Health Period Get',
940x8035: 'Config Health Period Set',
950x8036: 'Config Health Period Set Unacknowledged',
960x8037: 'Config Health Period Status',
970x8038: 'Config Heartbeat Publication Get',
980x8039: 'Config Heartbeat Publication Set',
990x803a: 'Config Heartbeat Subscription Get',
1000x803b: 'Config Heartbeat Subscription Set',
1010x803c: 'Config Heartbeat Subscription Status',
1020x803d: 'Config Model App Bind',
1030x803e: 'Config Model App Status',
1040x803f: 'Config Model App Unbind',
1050x8040: 'Config Netkey Add',
1060x8041: 'Config Netkey Delete',
1070x8042: 'Config Netkey Get',
1080x8043: 'Config Netkey List',
1090x8044: 'Config Netkey Status',
1100x8045: 'Config Netkey Update',
1110x8046: 'Config Node Identity Get',
1120x8047: 'Config Node Identity Set',
1130x8048: 'Config Node Identity Status',
1140x8049: 'Config Node Reset',
1150x804a: 'Config Node Reset Status',
1160x804b: 'Config Sig Model App Get',
1170x804c: 'Config Sig Model App List',
1180x804d: 'Config Vendor Model App Get',
1190x804e: 'Config Vendor Model App List',
120# Generic
1210x8201: 'Generic On Off Get',
1220x8202: 'Generic On Off Set',
1230x8203: 'Generic On Off Set Unacknowledged',
1240x8204: 'Generic On Off Status',
1250x8205: 'Generic Level Get',
1260x8206: 'Generic Level Set',
1270x8207: 'Generic Level Set Unacknowledged',
1280x8208: 'Generic Level Status',
1290x8209: 'Generic Delta Set',
1300x820A: 'Generic Delta Set Unacknowledged',
1310x820B: 'Generic Move Set',
1320x820C: 'Generic Move Set Unacknowledged',
133}
134
135# quick hack to convert Access message opcode + names
136tmp_messages =[
137# 'GENERIC_ON_OFF_GET                     :0x8201',
138]
139for message in tmp_messages:
140    parts = message.split(':')
141    print ("%s: '%s'," % (parts[1].strip(),parts[0].strip().title().replace('_',' ')))
142
143# helpers
144def read_net_32_from_file(f):
145    data = f.read(4)
146    if len(data) < 4:
147        return -1
148    return struct.unpack('>I', data)[0]
149
150def as_hex(data):
151    return ''.join(["{0:02x} ".format(byte) for byte in data])
152
153def as_big_endian32(value):
154    return struct.pack('>I', value)
155
156def read_net_16(data):
157    return struct.unpack('>H', data)[0]
158
159def read_net_24(data):
160    return data[0] << 16 | struct.unpack('>H', data[1:3])[0]
161
162# log engine - simple pretty printer
163max_indent = 0
164def log_pdu(pdu, indent = 0, hide_properties = []):
165    spaces = '    ' * indent
166    print(spaces + "%-20s %s" % (pdu.type, pdu.summary))
167    if indent >= max_indent:
168        return
169    for property in pdu.properties:
170        if property.key in hide_properties:
171            continue
172        if isinstance( property.value, int):
173            print (spaces + "|%20s: 0x%x (%u)" % (property.key, property.value, property.value))
174        elif isinstance( property.value, bytes):
175            print (spaces + "|%20s: %s" % (property.key, as_hex(property.value)))
176        else:
177            print (spaces + "|%20s: %s" % (property.key, str(property.value)))
178        hide_properties.append(property.key)
179    print (spaces + '|           data: ' + as_hex(pdu.data))
180    print (spaces + '----')
181    for origin in pdu.origins:
182        log_pdu(origin, indent + 1, hide_properties)
183
184# classes
185
186class network_key(object):
187    def __init__(self, index, netkey):
188        self.index = index
189        self.netkey = netkey
190        (self.nid, self.encryption_key, self.privacy_key) = k2(netkey, b'\x00')
191
192    def __repr__(self):
193        return ("NetKey-%04x %s: NID %02x Encryption %s Privacy %s" % (self.index, self.netkey.hex(), self.nid, self.encryption_key.hex(), self.privacy_key.hex()))
194
195class application_key(object):
196    def __init__(self, index, appkey):
197        self.index = index
198        self.appkey = appkey
199        self.aid = k4(self.appkey)
200
201    def __repr__(self):
202        return ("AppKey-%04x %s: AID %02x" % (self.index, self.appkey.hex(), self.aid))
203
204class property(object):
205    def __init__(self, key, value):
206        self.key = key
207        self.value = value
208
209class layer_pdu(object):
210    def __init__(self, pdu_type, pdu_data):
211        self.summary = ''
212        self.src = None
213        self.dst = None
214        self.type = pdu_type
215        self.data = pdu_data
216        self.origins = []
217        self.properties = []
218
219    def add_property(self, key, value):
220        self.properties.append(property(key, value))
221
222class network_pdu(layer_pdu):
223    def __init__(self, pdu_data):
224        super().__init__("Network(unencrpyted)", pdu_data)
225
226        # parse pdu
227        self.ivi = (self.data[1] & 0x80) >> 7
228        self.nid = self.data[0] & 0x7f
229        self.ctl = (self.data[1] & 0x80) == 0x80
230        self.ttl = self.data[1] & 0x7f
231        self.seq = read_net_24(self.data[2:5])
232        self.src = read_net_16(self.data[5:7])
233        self.dst = read_net_16(self.data[7:9])
234        self.lower_transport = self.data[9:]
235
236        # set properties
237        self.add_property('ivi', self.ivi)
238        self.add_property('nid', self.nid)
239        self.add_property('ctl', self.ctl)
240        self.add_property('ttl', self.ttl)
241        self.add_property('seq', self.seq)
242        self.add_property('src', self.src)
243        self.add_property('dst', self.dst)
244        self.add_property('lower_transport', self.lower_transport)
245
246class lower_transport_pdu(layer_pdu):
247    def __init__(self, network_pdu):
248        super().__init__('Lower Transport', network_pdu.lower_transport)
249
250        # inherit properties
251        self.ctl = network_pdu.ctl
252        self.seq = network_pdu.seq
253        self.src = network_pdu.src
254        self.dst = network_pdu.dst
255        self.add_property('ctl', self.ctl)
256        self.add_property('seq', self.seq)
257        self.add_property('src', self.src)
258        self.add_property('dst', self.dst)
259
260        # parse pdu and set propoerties
261        self.seg = (self.data[0] & 0x80) == 0x80
262        self.add_property('seg', self.seg)
263        self.szmic = False
264        if self.ctl:
265            self.opcode = self.data[0] & 0x7f
266            self.add_property('opcode', self.opcode)
267        else:
268            self.aid = self.data[0] & 0x3f
269            self.add_property('aid', self.aid)
270            self.akf = self.data[0] & 0x40 == 0x040
271            self.add_property('akf', self.akf)
272        if self.seg:
273            if not self.ctl:
274                self.szmic = self.data[1] & 0x80 == 0x80
275                self.add_property('szmic', self.szmic)
276            temp_12 = struct.unpack('>H', self.data[1:3])[0]
277            self.seq_zero = (temp_12 >> 2) & 0x1fff
278            self.add_property('seq_zero', self.seq_zero)
279            temp_23 = struct.unpack('>H', self.data[2:4])[0]
280            self.seg_o = (temp_23 >> 5) & 0x1f
281            self.add_property('seg_o', self.seg_o)
282            self.seg_n =  temp_23 & 0x1f
283            self.add_property('seg_n', self.seg_n)
284            self.segment = self.data[4:]
285            self.add_property('segment', self.segment)
286        else:
287            self.seq_auth = self.seq
288            self.upper_transport = self.data[1:]
289            self.add_property('upper_transport', self.upper_transport)
290
291class upper_transport_pdu(layer_pdu):
292    def __init__(self, segment):
293        if segment.ctl:
294            super().__init__('Segmented Control', b'')
295        else:
296            super().__init__('Segmented Transport', b'')
297        self.ctl      = segment.ctl
298        self.src      = segment.src
299        self.dst      = segment.dst
300        self.seq      = segment.seq
301        self.akf      = segment.akf
302        self.aid      = segment.aid
303        self.szmic    = segment.szmic
304        self.seg_n    = segment.seg_n
305        self.seq_zero = segment.seq_zero
306        # TODO handle seq_zero overrun
307        self.seq_auth = segment.seq & 0xffffe000 | segment.seq_zero
308        self.add_property('seq_auth', self.seq_auth)
309        self.missing  = (1 << (segment.seg_n+1)) - 1
310        self.data = b''
311        self.processed = False
312        self.origins = []
313        if self.ctl:
314            self.segment_len = 8
315        else:
316            self.segment_len = 12
317
318        self.add_property('src', self.src)
319        self.add_property('dst', self.dst)
320        self.add_property('aid', self.aid)
321        self.add_property('akf', self.akf)
322        self.add_property('segment_len', self.segment_len)
323
324    def add_segment(self, network_pdu):
325        self.origins.append(network_pdu)
326        self.missing &= ~ (1 << network_pdu.seg_o)
327        if network_pdu.seg_o == self.seg_n:
328            # last segment, set len
329            self.len = (self.seg_n * self.segment_len) + len(network_pdu.segment)
330        if len(self.data) == 0 and self.complete():
331            self.reassemble()
332
333    def complete(self):
334        return self.missing == 0
335
336    def reassemble(self):
337        self.data = bytearray(self.len)
338        missing  = (1 << (self.seg_n+1)) - 1
339        for pdu in self.origins:
340            # copy data
341            pos = pdu.seg_o * self.segment_len
342            self.data[pos:pos+len(pdu.segment)] = pdu.segment
343            # done?
344            missing &= ~ (1 << pdu.seg_o)
345            if missing == 0:
346                break
347
348class access_pdu(layer_pdu):
349    def __init__(self, lower_pdu, data):
350        super().__init__('Access', b'')
351        self.src      = lower_pdu.src
352        self.dst      = lower_pdu.dst
353        self.akf      = lower_pdu.akf
354        self.aid      = lower_pdu.aid
355        self.seq_auth = lower_pdu.seq_auth
356        self.data     = data
357        self.add_property('src', self.src)
358        self.add_property('dst', self.dst)
359        self.add_property('akf', self.akf)
360        self.add_property('aid', self.aid)
361        self.add_property('seq_auth', self.seq_auth)
362
363        upper_bits = data[0] >> 6
364        if upper_bits == 0 or upper_bits == 1:
365            self.opcode = data[0]
366            self.opcode_len = 1
367        elif upper_bits == 2:
368            self.opcode = read_net_16(data[0:2])
369            self.opcode_len = 2
370        elif upper_bits == 3:
371            self.opcode = read_net_24(data[0:3])
372            self.opcode_len = 3
373        self.add_property('opcode', self.opcode)
374        self.params = data[self.opcode_len:]
375        self.add_property('params', self.params)
376        if self.opcode in access_messages:
377            self.summary = access_messages[self.opcode]
378
379def segmented_message_for_pdu(pdu):
380    if pdu.src in segmented_messages:
381        seg_message = segmented_messages[pdu.src]
382        # check seq zero
383        if pdu.seq_zero  == seg_message.seq_zero:
384            return seg_message
385    # print("new segmented message: src %04x, seq_zero %04x" % (pdu.src, pdu.seq_zero))
386    seg_message = upper_transport_pdu(pdu)
387    segmented_messages[pdu.src] = seg_message
388    return seg_message
389
390def mesh_set_iv_index(iv_index):
391    global ivi
392    ivi = iv_index
393    print ("IV-Index: " + as_big_endian32(ivi).hex())
394
395# key management
396def mesh_add_netkey(index, netkey):
397    key = network_key(index, netkey)
398    print (key)
399    netkeys[index] = key
400
401def mesh_network_keys_for_nid(nid):
402    for (index, key) in netkeys.items():
403        if key.nid == nid:
404            yield key
405
406def mesh_set_device_key(key):
407    global devkey
408    print ("DevKey: " + key.hex())
409    devkey = key
410
411def mesh_add_application_key(index, appkey):
412    key = application_key(index, appkey)
413    print (key)
414    appkeys[index] = key
415
416def mesh_application_keys_for_aid(aid):
417    for (index, key) in appkeys.items():
418        if key.aid == aid:
419            yield key
420
421def mesh_transport_nonce(pdu, nonce_type):
422    if pdu.szmic:
423        aszmic = 0x80
424    else:
425        aszmic = 0x00
426    return bytes( [nonce_type, aszmic, pdu.seq_auth >> 16, (pdu.seq_auth >> 8) & 0xff, pdu.seq_auth & 0xff, pdu.src >> 8, pdu.src & 0xff, pdu.dst >> 8, pdu.dst & 0xff]) + as_big_endian32(ivi)
427
428def mesh_application_nonce(pdu):
429    return mesh_transport_nonce(pdu, 0x01)
430
431def mesh_device_nonce(pdu):
432    return mesh_transport_nonce(pdu, 0x02)
433
434def mesh_upper_transport_decrypt(message, data):
435    if message.szmic:
436        trans_mic_len = 8
437    else:
438        trans_mic_len = 4
439    ciphertext = data[:-trans_mic_len]
440    trans_mic  = data[-trans_mic_len:]
441    nonce      = mesh_device_nonce(message)
442    decrypted = None
443    if message.akf:
444        for key in mesh_application_keys_for_aid(message.aid):
445            decrypted = aes_ccm_decrypt(key.appkey, nonce, ciphertext, b'', trans_mic_len, trans_mic)
446            if decrypted != None:
447                break
448    else:
449        decrypted =  aes_ccm_decrypt(devkey, nonce, ciphertext, b'', trans_mic_len, trans_mic)
450    return decrypted
451
452def mesh_process_control(control_pdu):
453    # TODO decode control message
454    # TODO add Seg Ack to sender access message origins
455    log_pdu(control_pdu, 0, [])
456
457def mesh_process_access(access_pdu):
458    log_pdu(access_pdu, 0, [])
459
460def mesh_process_network_pdu_tx(network_pdu_encrypted):
461
462    # network layer - decrypt pdu
463    nid = network_pdu_encrypted.data[0] & 0x7f
464    for key in mesh_network_keys_for_nid(nid):
465        network_pdu_decrypted_data = network_decrypt(network_pdu_encrypted.data, as_big_endian32(ivi), key.encryption_key, key.privacy_key)
466        if network_pdu_decrypted_data != None:
467            break
468    if network_pdu_decrypted_data == None:
469        network_pdu_encrypted.summary = 'No encryption key found'
470        log_pdu(network_pdu_encrypted, 0, [])
471        return
472
473    # decrypted network pdu
474    network_pdu_decrypted = network_pdu(network_pdu_decrypted_data)
475    network_pdu_decrypted.origins.append(network_pdu_encrypted)
476
477    # print("network pdu (enc)" + network_pdu_encrypted.data.hex())
478    # print("network pdu (dec)" + network_pdu_decrypted_data.hex())
479
480    # lower transport - reassemble
481    lower_transport = lower_transport_pdu(network_pdu_decrypted)
482    lower_transport.origins.append(network_pdu_decrypted)
483
484    if lower_transport.seg:
485        message = segmented_message_for_pdu(lower_transport)
486        message.add_segment(lower_transport)
487        if not message.complete():
488            return
489        if message.processed:
490            return
491
492        message.processed = True
493        if message.ctl:
494            mesh_process_control(message)
495        else:
496            access_payload = mesh_upper_transport_decrypt(message, message.data)
497            if access_payload == None:
498                message.summary = 'No encryption key found'
499                log_pdu(message, 0, [])
500            else:
501                access = access_pdu(message, access_payload)
502                access.origins.append(message)
503                mesh_process_access(access)
504
505    else:
506        # print("lower_transport.ctl = " + str(lower_transport.ctl))
507        if lower_transport.ctl:
508            control = layer_pdu('Unsegmented Control', lower_transport.data)
509            control.origins.append(lower_transport)
510            mesh_process_control(control)
511        else:
512            access_payload = mesh_upper_transport_decrypt(lower_transport, lower_transport.upper_transport)
513            if access_payload == None:
514                lower_transport.summary = 'No encryption key found'
515                log_pdu(lower_transport, 0, [])
516            else:
517                access = access_pdu(lower_transport, access_payload)
518                access.add_property('seq_auth', lower_transport.seq)
519                access.origins.append(lower_transport)
520                mesh_process_access(access)
521
522
523def mesh_process_beacon_pdu(adv_pdu):
524    log_pdu(adv_pdu, 0, [])
525
526def mesh_process_adv(adv_pdu):
527    ad_len  = adv_pdu.data[0] - 1
528    ad_type = adv_pdu.data[1]
529    if ad_type == 0x2A:
530        network_pdu_encrypted = layer_pdu("Network(encrypted)", adv_data[2:2+ad_len])
531        network_pdu_encrypted.add_property('ivi', adv_data[2] >> 7)
532        network_pdu_encrypted.add_property('nid', adv_data[2] & 0x7f)
533        network_pdu_encrypted.origins.append(adv_pdu)
534        mesh_process_network_pdu_tx(network_pdu_encrypted)
535    if ad_type == 0x2b:
536        beacon_pdu = layer_pdu("Beacon", adv_data[2:2+ad_len])
537        beacon_pdu.origins.append(adv_pdu)
538        mesh_process_beacon_pdu(beacon_pdu)
539
540
541if len(sys.argv) == 1:
542    print ('Dump Mesh PacketLogger file')
543    print ('Copyright 2019, BlueKitchen GmbH')
544    print ('')
545    print ('Usage: ' + sys.argv[0] + 'hci_dump.pklg')
546    exit(0)
547
548infile = sys.argv[1]
549
550with open (infile, 'rb') as fin:
551    pos = 0
552    while True:
553        payload_length  = read_net_32_from_file(fin)
554        if payload_length < 0:
555            break
556        ts_sec  = read_net_32_from_file(fin)
557        ts_usec = read_net_32_from_file(fin)
558        type    = ord(fin.read(1))
559        packet_len = payload_length - 9;
560        if (packet_len > 66000):
561            print ("Error parsing pklg at offset %u (%x)." % (pos, pos))
562            break
563
564        packet  = fin.read(packet_len)
565        pos     = pos + 4 + payload_length
566        # time    = "[%s.%03u] " % (datetime.datetime.fromtimestamp(ts_sec).strftime("%Y-%m-%d %H:%M:%S"), ts_usec / 1000)
567
568        if type == 0:
569            # CMD
570            if packet[0] != 0x08:
571                continue
572            if packet[1] != 0x20:
573                continue
574            adv_data = packet[4:]
575            adv_pdu = layer_pdu("ADV(TX)", adv_data)
576            mesh_process_adv(adv_pdu)
577
578        elif type == 1:
579            # EVT
580            event = packet[0]
581            if event != 0x3e:
582                continue
583            event_len = packet[1]
584            if event_len < 14:
585                continue
586            adv_data = packet[13:-1]
587            adv_pdu = layer_pdu("ADV(RX)", adv_data)
588            mesh_process_adv(adv_pdu)
589
590        elif type == 0xfc:
591            # LOG
592            log = packet.decode("utf-8")
593            parts = re.match('mesh-iv-index: (.*)', log)
594            if parts and len(parts.groups()) == 1:
595                mesh_set_iv_index(int(parts.groups()[0], 16))
596                continue
597            parts = re.match('mesh-devkey: (.*)', log)
598            if parts and len(parts.groups()) == 1:
599                mesh_set_device_key(bytes.fromhex(parts.groups()[0]))
600                continue
601            parts = re.match('mesh-appkey-(.*): (.*)', log)
602            if parts and len(parts.groups()) == 2:
603                mesh_add_application_key(int(parts.groups()[0], 16), bytes.fromhex(parts.groups()[1]))
604                continue
605            parts = re.match('mesh-netkey-(.*): (.*)', log)
606            if parts and len(parts.groups()) == 2:
607                mesh_add_netkey(int(parts.groups()[0], 16), bytes.fromhex(parts.groups()[1]))
608                continue
609
610