xref: /btstack/test/mesh/dump_mesh_pklg.py (revision eed276207a52fcf1d60acef934ce5d9f7c40a7df)
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
32
33# helpers
34def read_net_32_from_file(f):
35    data = f.read(4)
36    if len(data) < 4:
37        return -1
38    return struct.unpack('>I', data)[0]
39
40def as_hex(data):
41    return ''.join(["{0:02x} ".format(byte) for byte in data])
42
43def as_big_endian32(value):
44    return struct.pack('>I', value)
45
46def read_net_16(data):
47    return struct.unpack('>H', data)[0]
48
49def read_net_24(data):
50    return data[0] << 16 | struct.unpack('>H', data[1:3])[0]
51
52# log engine - simple pretty printer
53max_indent = 0
54def log_pdu(pdu, indent = 0, hide_properties = []):
55    spaces = '    ' * indent
56    print(spaces + pdu.type)
57    if len(pdu.status) > 0:
58        print (spaces + '|           status: ' + pdu.status)
59    for property in pdu.properties:
60        if property.key in hide_properties:
61            continue
62        if isinstance( property.value, int):
63            print (spaces + "|%15s: 0x%x (%u)" % (property.key, property.value, property.value))
64        elif isinstance( property.value, bytes):
65            print (spaces + "|%15s: %s" % (property.key, as_hex(property.value)))
66        else:
67            print (spaces + "|%15s: %s" % (property.key, str(property.value)))
68        hide_properties.append(property.key)
69    print (spaces + '|           data: ' + as_hex(pdu.data))
70    if indent >= max_indent:
71        return
72    print (spaces + '----')
73    for origin in pdu.origins:
74        log_pdu(origin, indent + 1, hide_properties)
75
76# classes
77
78class network_key(object):
79    def __init__(self, index, netkey):
80        self.index = index
81        self.netkey = netkey
82        (self.nid, self.encryption_key, self.privacy_key) = k2(netkey, b'\x00')
83
84    def __repr__(self):
85        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()))
86
87class application_key(object):
88    def __init__(self, index, appkey):
89        self.index = index
90        self.appkey = appkey
91        self.aid = k4(self.appkey)
92
93    def __repr__(self):
94        return ("AppKey-%04x %s: AID %02x" % (self.index, self.appkey.hex(), self.aid))
95
96class property(object):
97    def __init__(self, key, value):
98        self.key = key
99        self.value = value
100
101class layer_pdu(object):
102    def __init__(self, pdu_type, pdu_data):
103        self.status = ''
104        self.src = None
105        self.dst = None
106        self.type = pdu_type
107        self.data = pdu_data
108        self.origins = []
109        self.properties = []
110
111    def add_property(self, key, value):
112        self.properties.append(property(key, value))
113
114class network_pdu(layer_pdu):
115    def __init__(self, pdu_data):
116        super().__init__("Network(unencrpyted)", pdu_data)
117
118        # parse pdu
119        self.ivi = (self.data[1] & 0x80) >> 7
120        self.nid = self.data[0] & 0x7f
121        self.ctl = (self.data[1] & 0x80) == 0x80
122        self.ttl = self.data[1] & 0x7f
123        self.seq = read_net_24(self.data[2:5])
124        self.src = read_net_16(self.data[5:7])
125        self.dst = read_net_16(self.data[7:9])
126        self.lower_transport = self.data[9:]
127
128        # set properties
129        self.add_property('ivi', self.ivi)
130        self.add_property('nid', self.nid)
131        self.add_property('ctl', self.ctl)
132        self.add_property('ttl', self.ttl)
133        self.add_property('seq', self.seq)
134        self.add_property('src', self.src)
135        self.add_property('dst', self.dst)
136        self.add_property('lower_transport', self.lower_transport)
137
138class lower_transport_pdu(layer_pdu):
139    def __init__(self, network_pdu):
140        super().__init__('Lower Transport', network_pdu.lower_transport)
141
142        # inherit properties
143        self.ctl = network_pdu.ctl
144        self.seq = network_pdu.seq
145        self.src = network_pdu.src
146        self.dst = network_pdu.dst
147        self.add_property('ctl', self.ctl)
148        self.add_property('seq', self.seq)
149        self.add_property('src', self.src)
150        self.add_property('dst', self.dst)
151
152        # parse pdu and set propoerties
153        self.seg = (self.data[0] & 0x80) == 0x80
154        self.add_property('seg', self.seg)
155        self.szmic = False
156        if self.ctl:
157            self.opcode = self.data[0] & 0x7f
158            self.add_property('opcode', self.opcode)
159        else:
160            self.aid = self.data[0] & 0x3f
161            self.add_property('aid', self.aid)
162            self.akf = self.data[0] & 0x40 == 0x040
163            self.add_property('akf', self.akf)
164        if self.seg:
165            if not self.ctl:
166                self.szmic = self.data[1] & 0x80 == 0x80
167                self.add_property('szmic', self.szmic)
168            temp_12 = struct.unpack('>H', self.data[1:3])[0]
169            self.seq_zero = (temp_12 >> 2) & 0x1fff
170            self.add_property('seq_zero', self.seq_zero)
171            temp_23 = struct.unpack('>H', self.data[2:4])[0]
172            self.seg_o = (temp_23 >> 5) & 0x1f
173            self.add_property('seg_o', self.seg_o)
174            self.seg_n =  temp_23 & 0x1f
175            self.add_property('seg_n', self.seg_n)
176            self.segment = self.data[4:]
177            self.add_property('segment', self.segment)
178        else:
179            self.seq_auth = self.seq
180            self.upper_transport = self.data[1:]
181            self.add_property('upper_transport', self.upper_transport)
182
183class upper_transport_pdu(layer_pdu):
184    def __init__(self, segment):
185        if segment.ctl:
186            super().__init__('Segmented Control', b'')
187        else:
188            super().__init__('Segmented Transport', b'')
189        self.ctl      = segment.ctl
190        self.src      = segment.src
191        self.dst      = segment.dst
192        self.seq      = segment.seq
193        self.akf      = segment.akf
194        self.aid      = segment.aid
195        self.szmic    = segment.szmic
196        self.seg_n    = segment.seg_n
197        self.seq_zero = segment.seq_zero
198        # TODO handle seq_zero overrun
199        self.seq_auth = segment.seq & 0xffffe000 | segment.seq_zero
200        self.add_property('seq_auth', self.seq_auth)
201        self.missing  = (1 << (segment.seg_n+1)) - 1
202        self.data = b''
203        self.processed = False
204        self.origins = []
205        if self.ctl:
206            self.segment_len = 8
207        else:
208            self.segment_len = 12
209
210        self.add_property('src', self.src)
211        self.add_property('dst', self.dst)
212        self.add_property('aid', self.aid)
213        self.add_property('akf', self.akf)
214        self.add_property('segment_len', self.segment_len)
215
216    def add_segment(self, network_pdu):
217        self.origins.append(network_pdu)
218        self.missing &= ~ (1 << network_pdu.seg_o)
219        if network_pdu.seg_o == self.seg_n:
220            # last segment, set len
221            self.len = (self.seg_n * self.segment_len) + len(network_pdu.segment)
222        if len(self.data) == 0 and self.complete():
223            self.reassemble()
224
225    def complete(self):
226        return self.missing == 0
227
228    def reassemble(self):
229        self.data = bytearray(self.len)
230        missing  = (1 << (self.seg_n+1)) - 1
231        for pdu in self.origins:
232            # copy data
233            pos = pdu.seg_o * self.segment_len
234            self.data[pos:pos+len(pdu.segment)] = pdu.segment
235            # done?
236            missing &= ~ (1 << pdu.seg_o)
237            if missing == 0:
238                break
239
240class access_pdu(layer_pdu):
241    def __init__(self, lower_pdu, data):
242        super().__init__('Access', b'')
243        self.src      = lower_pdu.src
244        self.dst      = lower_pdu.dst
245        self.akf      = lower_pdu.akf
246        self.aid      = lower_pdu.aid
247        self.seq_auth = lower_pdu.seq_auth
248        self.data     = data
249        self.add_property('src', self.src)
250        self.add_property('dst', self.dst)
251        self.add_property('akf', self.akf)
252        self.add_property('aid', self.aid)
253        self.add_property('seq_auth', self.seq_auth)
254
255def segmented_message_for_pdu(pdu):
256    if pdu.src in segmented_messages:
257        seg_message = segmented_messages[pdu.src]
258        # check seq zero
259        if pdu.seq_zero  == seg_message.seq_zero:
260            return seg_message
261    # print("new segmented message: src %04x, seq_zero %04x" % (pdu.src, pdu.seq_zero))
262    seg_message = upper_transport_pdu(pdu)
263    segmented_messages[pdu.src] = seg_message
264    return seg_message
265
266def mesh_set_iv_index(iv_index):
267    global ivi
268    ivi = iv_index
269    print ("IV-Index: " + as_big_endian32(ivi).hex())
270
271# key management
272def mesh_add_netkey(index, netkey):
273    key = network_key(index, netkey)
274    print (key)
275    netkeys[index] = key
276
277def mesh_network_keys_for_nid(nid):
278    for (index, key) in netkeys.items():
279        if key.nid == nid:
280            yield key
281
282def mesh_set_device_key(key):
283    global devkey
284    print ("DevKey: " + key.hex())
285    devkey = key
286
287def mesh_add_application_key(index, appkey):
288    key = application_key(index, appkey)
289    print (key)
290    appkeys[index] = key
291
292def mesh_application_keys_for_aid(aid):
293    for (index, key) in appkeys.items():
294        if key.aid == aid:
295            yield key
296
297def mesh_transport_nonce(pdu, nonce_type):
298    if pdu.szmic:
299        aszmic = 0x80
300    else:
301        aszmic = 0x00
302    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)
303
304def mesh_application_nonce(pdu):
305    return mesh_transport_nonce(pdu, 0x01)
306
307def mesh_device_nonce(pdu):
308    return mesh_transport_nonce(pdu, 0x02)
309
310def mesh_upper_transport_decrypt(message, data):
311    if message.szmic:
312        trans_mic_len = 8
313    else:
314        trans_mic_len = 4
315    ciphertext = data[:-trans_mic_len]
316    trans_mic  = data[-trans_mic_len:]
317    nonce      = mesh_device_nonce(message)
318    decrypted = None
319    if message.akf:
320        for key in mesh_application_keys_for_aid(message.aid):
321            decrypted = aes_ccm_decrypt(key.appkey, nonce, ciphertext, b'', trans_mic_len, trans_mic)
322            if decrypted != None:
323                break
324    else:
325        decrypted =  aes_ccm_decrypt(devkey, nonce, ciphertext, b'', trans_mic_len, trans_mic)
326    return decrypted
327
328def mesh_process_control(control_pdu):
329    # TODO decode control message
330    # TODO add Seg Ack to sender access message origins
331    log_pdu(control_pdu, 0, [])
332
333def mesh_process_access(access_pdu):
334    log_pdu(access_pdu, 0, [])
335
336def mesh_process_network_pdu_tx(network_pdu_encrypted):
337
338    # network layer - decrypt pdu
339    nid = network_pdu_encrypted.data[0] & 0x7f
340    for key in mesh_network_keys_for_nid(nid):
341        network_pdu_decrypted_data = network_decrypt(network_pdu_encrypted.data, as_big_endian32(ivi), key.encryption_key, key.privacy_key)
342        if network_pdu_decrypted_data != None:
343            break
344    if network_pdu_decrypted_data == None:
345        network_pdu_encrypted.status = 'No encryption key found'
346        log_pdu(network_pdu_encrypted, 0, [])
347        return
348
349    # decrypted network pdu
350    network_pdu_decrypted = network_pdu(network_pdu_decrypted_data)
351    network_pdu_decrypted.origins.append(network_pdu_encrypted)
352
353    # print("network pdu (enc)" + network_pdu_encrypted.data.hex())
354    # print("network pdu (dec)" + network_pdu_decrypted_data.hex())
355
356    # lower transport - reassemble
357    lower_transport = lower_transport_pdu(network_pdu_decrypted)
358    lower_transport.origins.append(network_pdu_decrypted)
359
360    if lower_transport.seg:
361        message = segmented_message_for_pdu(lower_transport)
362        message.add_segment(lower_transport)
363        if not message.complete():
364            return
365        if message.processed:
366            return
367
368        message.processed = True
369        if message.ctl:
370            mesh_process_control(message)
371        else:
372            access_payload = mesh_upper_transport_decrypt(message, message.data)
373            if access_payload == None:
374                message.status = 'No encryption key found'
375                log_pdu(message, 0, [])
376            else:
377                access = access_pdu(message, access_payload)
378                access.origins.append(message)
379                mesh_process_access(access)
380
381    else:
382        # print("lower_transport.ctl = " + str(lower_transport.ctl))
383        if lower_transport.ctl:
384            control = layer_pdu('Unsegmented Control', lower_transport.data)
385            control.origins.append(lower_transport)
386            mesh_process_control(control)
387        else:
388            access_payload = mesh_upper_transport_decrypt(lower_transport, lower_transport.upper_transport)
389            if access_payload == None:
390                lower_transport.status = 'No encryption key found'
391                log_pdu(lower_transport, 0, [])
392            else:
393                access = access_pdu(lower_transport, access_payload)
394                access.add_property('seq_auth', lower_transport.seq)
395                access.origins.append(lower_transport)
396                mesh_process_access(access)
397
398
399def mesh_process_beacon_pdu(adv_pdu):
400    log_pdu(adv_pdu, 0, [])
401
402def mesh_process_adv(adv_pdu):
403    ad_len  = adv_pdu.data[0] - 1
404    ad_type = adv_pdu.data[1]
405    if ad_type == 0x2A:
406        network_pdu_encrypted = layer_pdu("Network(encrypted)", adv_data[2:2+ad_len])
407        network_pdu_encrypted.add_property('ivi', adv_data[2] >> 7)
408        network_pdu_encrypted.add_property('nid', adv_data[2] & 0x7f)
409        network_pdu_encrypted.origins.append(adv_pdu)
410        mesh_process_network_pdu_tx(network_pdu_encrypted)
411    if ad_type == 0x2b:
412        beacon_pdu = layer_pdu("Beacon", adv_data[2:2+ad_len])
413        beacon_pdu.origins.append(adv_pdu)
414        mesh_process_beacon_pdu(beacon_pdu)
415
416
417if len(sys.argv) == 1:
418    print ('Dump Mesh PacketLogger file')
419    print ('Copyright 2019, BlueKitchen GmbH')
420    print ('')
421    print ('Usage: ' + sys.argv[0] + 'hci_dump.pklg')
422    exit(0)
423
424infile = sys.argv[1]
425
426with open (infile, 'rb') as fin:
427    pos = 0
428    while True:
429        payload_length  = read_net_32_from_file(fin)
430        if payload_length < 0:
431            break
432        ts_sec  = read_net_32_from_file(fin)
433        ts_usec = read_net_32_from_file(fin)
434        type    = ord(fin.read(1))
435        packet_len = payload_length - 9;
436        if (packet_len > 66000):
437            print ("Error parsing pklg at offset %u (%x)." % (pos, pos))
438            break
439
440        packet  = fin.read(packet_len)
441        pos     = pos + 4 + payload_length
442        # time    = "[%s.%03u] " % (datetime.datetime.fromtimestamp(ts_sec).strftime("%Y-%m-%d %H:%M:%S"), ts_usec / 1000)
443
444        if type == 0:
445            # CMD
446            if packet[0] != 0x08:
447                continue
448            if packet[1] != 0x20:
449                continue
450            adv_data = packet[4:]
451            adv_pdu = layer_pdu("ADV", adv_data)
452            mesh_process_adv(adv_pdu)
453
454        elif type == 1:
455            # EVT
456            event = packet[0]
457            if event != 0x3e:
458                continue
459            event_len = packet[1]
460            if event_len != 0x2b:
461                continue
462            adv_data = packet[13:-1]
463            adv_pdu = layer_pdu("ADV", adv_data)
464            mesh_process_adv(adv_pdu)
465
466        elif type == 0xfc:
467            # LOG
468            log = packet.decode("utf-8")
469            parts = re.match('mesh-iv-index: (.*)', log)
470            if parts and len(parts.groups()) == 1:
471                mesh_set_iv_index(int(parts.groups()[0], 16))
472                continue
473            parts = re.match('mesh-devkey: (.*)', log)
474            if parts and len(parts.groups()) == 1:
475                mesh_set_device_key(bytes.fromhex(parts.groups()[0]))
476                continue
477            parts = re.match('mesh-appkey-(.*): (.*)', log)
478            if parts and len(parts.groups()) == 2:
479                mesh_add_application_key(int(parts.groups()[0], 16), bytes.fromhex(parts.groups()[1]))
480                continue
481            parts = re.match('mesh-netkey-(.*): (.*)', log)
482            if parts and len(parts.groups()) == 2:
483                mesh_add_netkey(int(parts.groups()[0], 16), bytes.fromhex(parts.groups()[1]))
484                continue
485
486