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