meshcore 2.2.31__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcore-2.2.31 → meshcore-2.3.1}/PKG-INFO +1 -1
- {meshcore-2.2.31 → meshcore-2.3.1}/pyproject.toml +1 -1
- meshcore-2.3.1/src/meshcore/meshcore_parser.py +190 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/reader.py +25 -171
- meshcore-2.3.1/tests/unit/test_path_discovery_response.py +45 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/.github/python-test.yml +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/.gitignore +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/LICENSE +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/README.md +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/.pubsub_example.py.swp +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_chat.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_pin_pairing_example.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_private_key_export.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_sign_example.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_stats.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_chan_msg.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_custom_vars.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_infos.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_msg.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_msg_retries.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_set_cv.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/chan_recv_with_path.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/connection_events_example.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/mepo_mc_gps.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/pubsub_example.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/rf_packet_monitor.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_battery_monitor.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_channel_manager.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_chat.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_contacts.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_infos.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_meshcore_ollama.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_msg.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_pingbot.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_repeater_status.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_repeater_telemetry.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_rss_bot.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_trace.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_chat.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_login_status.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_contacts.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_infos.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_msg.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_readmsgs.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/pytest.ini +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/__init__.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/ble_cx.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/__init__.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/base.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/binary.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/contact.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/control_data.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/device.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/messaging.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/connection_manager.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/events.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/lpp_json_encoder.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/meshcore.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/packets.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/parsing.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/serial_cx.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/tcp_cx.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/README.md +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_ble_connection.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_ble_pin_pairing.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_meshcore_ble_pin.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_commands.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_events.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_private_key_export.py +0 -0
- {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_reader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Base classes for communicating with meshcore companion radios
|
|
5
5
|
Project-URL: Homepage, https://github.com/fdlamotte/meshcore_py
|
|
6
6
|
Project-URL: Issues, https://github.com/fdlamotte/meshcore_py/issues
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import io
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
from Crypto.Cipher import AES
|
|
7
|
+
from Crypto.Hash import HMAC, SHA256
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("meshcore")
|
|
10
|
+
|
|
11
|
+
PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
|
|
12
|
+
ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
|
|
13
|
+
CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
|
|
14
|
+
|
|
15
|
+
class MeshcorePacketParser:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.channels = [{} for _ in range(40)] # keep our own copy of channels, 40 elements by default
|
|
18
|
+
self.channels_log = [] # stores the channel msg events
|
|
19
|
+
self.decrypt_channels = False
|
|
20
|
+
|
|
21
|
+
async def newChannel(self, chan):
|
|
22
|
+
idx = chan['channel_idx']
|
|
23
|
+
if len(self.channels) <= idx:
|
|
24
|
+
self.channels.extend([{} for _ in range(1 + idx - len(self.channels))])
|
|
25
|
+
self.channels[idx] = chan
|
|
26
|
+
|
|
27
|
+
async def findLogChannelMsg(self, msg_hash):
|
|
28
|
+
# search for the same packet
|
|
29
|
+
return next((l for l in reversed(self.channels_log) if 'msg_hash' in l and l['msg_hash'] == msg_hash), None)
|
|
30
|
+
|
|
31
|
+
async def findLogChannelPkt(self, pkt_hash):
|
|
32
|
+
# search for the same packet
|
|
33
|
+
return next((l for l in reversed(self.channels_log) if 'pkt_hash' in l and l['pkt_hash'] == pkt_hash), None)
|
|
34
|
+
|
|
35
|
+
async def parsePacketPayload(self, payload, log_data={}):
|
|
36
|
+
""" Parses the payload of a log_rx packet
|
|
37
|
+
|
|
38
|
+
Parameters :
|
|
39
|
+
log_data: already gathered log_data we'll expand
|
|
40
|
+
payload: payload of the packet to analyze
|
|
41
|
+
|
|
42
|
+
Returns :
|
|
43
|
+
completed log_data
|
|
44
|
+
"""
|
|
45
|
+
pbuf = io.BytesIO(payload)
|
|
46
|
+
|
|
47
|
+
header = pbuf.read(1)[0]
|
|
48
|
+
route_type = header & 0x03
|
|
49
|
+
payload_type = (header & 0x3c) >> 2
|
|
50
|
+
payload_ver = (header & 0xc0) >> 6
|
|
51
|
+
|
|
52
|
+
transport_code = None
|
|
53
|
+
if route_type == 0x00 or route_type == 0x03: # has transport code
|
|
54
|
+
transport_code = pbuf.read(4) # discard transport code
|
|
55
|
+
|
|
56
|
+
path_byte = pbuf.read(1)[0]
|
|
57
|
+
path_hash_size = ((path_byte & 0xC0) >> 6) + 1
|
|
58
|
+
path_len = (path_byte & 0x3F)
|
|
59
|
+
# here path_len is number of hops, not number of bytes
|
|
60
|
+
|
|
61
|
+
path = pbuf.read(path_len*path_hash_size).hex() # Beware of traces where pathes are mixed
|
|
62
|
+
|
|
63
|
+
try :
|
|
64
|
+
route_typename = ROUTE_TYPENAMES[route_type]
|
|
65
|
+
except IndexError:
|
|
66
|
+
logger.debug(f"Unknown route type {route_type}")
|
|
67
|
+
route_typename = "UNK"
|
|
68
|
+
|
|
69
|
+
try :
|
|
70
|
+
payload_typename = PAYLOAD_TYPENAMES[payload_type]
|
|
71
|
+
except IndexError:
|
|
72
|
+
logger.debug(f"Unknown payload type {payload_type}")
|
|
73
|
+
payload_typename = "UNK"
|
|
74
|
+
|
|
75
|
+
pkt_payload = pbuf.read()
|
|
76
|
+
pkt_hash = int.from_bytes(SHA256.new(pkt_payload).digest()[0:4], "little", signed=False)
|
|
77
|
+
|
|
78
|
+
log_data["header"] = header
|
|
79
|
+
log_data["route_type"] = route_type
|
|
80
|
+
log_data["route_typename"] = route_typename
|
|
81
|
+
log_data["payload_type"] = payload_type
|
|
82
|
+
log_data["payload_typename"]= payload_typename
|
|
83
|
+
|
|
84
|
+
log_data["payload_ver"] = payload_ver
|
|
85
|
+
|
|
86
|
+
if not transport_code is None:
|
|
87
|
+
log_data["transport_code"] = transport_code.hex()
|
|
88
|
+
|
|
89
|
+
log_data["path_len"] = path_len
|
|
90
|
+
log_data["path_hash_size"] = path_hash_size
|
|
91
|
+
log_data["path"] = path
|
|
92
|
+
|
|
93
|
+
log_data["pkt_payload"] = pkt_payload
|
|
94
|
+
log_data["pkt_hash"] = pkt_hash
|
|
95
|
+
|
|
96
|
+
if not payload is None and payload_type == 0x05: # flood msg / channel
|
|
97
|
+
pk_buf = io.BytesIO(pkt_payload)
|
|
98
|
+
chan_hash = pk_buf.read(1).hex()
|
|
99
|
+
cipher_mac = pk_buf.read(2)
|
|
100
|
+
msg = pk_buf.read() # until the end of buffer
|
|
101
|
+
|
|
102
|
+
channel = None
|
|
103
|
+
for c in self.channels:
|
|
104
|
+
if "channel_hash" in c and c["channel_hash"] == chan_hash : # validate against MAC
|
|
105
|
+
h = HMAC.new(c["channel_secret"], digestmod=SHA256)
|
|
106
|
+
h.update(msg)
|
|
107
|
+
if h.digest()[0:2] == cipher_mac:
|
|
108
|
+
channel = c
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
chan_name = ""
|
|
112
|
+
|
|
113
|
+
if channel is None :
|
|
114
|
+
chan_name = chan_hash
|
|
115
|
+
else:
|
|
116
|
+
chan_name = channel["channel_name"]
|
|
117
|
+
|
|
118
|
+
log_data["chan_hash"] = chan_hash
|
|
119
|
+
log_data["cipher_mac"] = cipher_mac.hex()
|
|
120
|
+
log_data["crypted"] = msg.hex()
|
|
121
|
+
log_data["chan_name"] = chan_name
|
|
122
|
+
|
|
123
|
+
if not channel is None and self.decrypt_channels:
|
|
124
|
+
|
|
125
|
+
logged = await self.findLogChannelPkt(pkt_hash)
|
|
126
|
+
|
|
127
|
+
if logged is None:
|
|
128
|
+
# not found: decrypt the text and hash it
|
|
129
|
+
aes_key = channel["channel_secret"]
|
|
130
|
+
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
131
|
+
uncrypted = cipher.decrypt(msg)
|
|
132
|
+
timestamp = int.from_bytes(uncrypted[0:4], "little", signed=False)
|
|
133
|
+
attempt = uncrypted[4] & 3
|
|
134
|
+
txt_type = int.from_bytes(uncrypted[4:4], "little", signed=False) >> 2
|
|
135
|
+
message = uncrypted[5:].strip(b"\0")
|
|
136
|
+
msg_hash = int.from_bytes(SHA256.new(timestamp.to_bytes(4, "little", signed=False) + message).digest()[0:4], "little", signed=False)
|
|
137
|
+
log_data["message"] = message.decode("utf-8", "ignore")
|
|
138
|
+
log_data["msg_hash"] = msg_hash
|
|
139
|
+
log_data["sender_timestamp"] = timestamp
|
|
140
|
+
log_data["attempt"] = attempt
|
|
141
|
+
log_data["txt_type"] = txt_type
|
|
142
|
+
else:
|
|
143
|
+
# found: copy
|
|
144
|
+
log_data["message"] = logged["message"]
|
|
145
|
+
log_data["msg_hash"] = logged["msg_hash"]
|
|
146
|
+
log_data["sender_timestamp"] = logged["sender_timestamp"]
|
|
147
|
+
log_data["attempt"] = logged["attempt"]
|
|
148
|
+
log_data["txt_type"] = logged["txt_type"]
|
|
149
|
+
|
|
150
|
+
self.channels_log.append(log_data)
|
|
151
|
+
if len(self.channels_log) > 100:
|
|
152
|
+
del self.channels_log[:25]
|
|
153
|
+
|
|
154
|
+
elif not payload is None and payload_type == 0x04: # Advert
|
|
155
|
+
pk_buf = io.BytesIO(pkt_payload)
|
|
156
|
+
adv_key = pk_buf.read(32).hex()
|
|
157
|
+
adv_timestamp = int.from_bytes(pk_buf.read(4), "little", signed=False)
|
|
158
|
+
signature = pk_buf.read(64).hex()
|
|
159
|
+
flags = pk_buf.read(1)[0]
|
|
160
|
+
adv_type = flags & 0x0F
|
|
161
|
+
adv_lat = None
|
|
162
|
+
adv_lon = None
|
|
163
|
+
adv_feat1 = None
|
|
164
|
+
adv_feat2 = None
|
|
165
|
+
if flags & 0x10 > 0: #has location
|
|
166
|
+
adv_lat = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
|
|
167
|
+
adv_lon = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
|
|
168
|
+
if flags & 0x20 > 0: #has feature1
|
|
169
|
+
adv_feat1 = pk_buf.read(2).hex()
|
|
170
|
+
if flags & 0x40 > 0: #has feature2
|
|
171
|
+
adv_feat2 = pk_buf.read(2).hex()
|
|
172
|
+
if flags & 0x80 > 0: #has name
|
|
173
|
+
adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
|
|
174
|
+
log_data["adv_name"] = adv_name
|
|
175
|
+
|
|
176
|
+
log_data["adv_key"] = adv_key
|
|
177
|
+
log_data["adv_timestamp"] = adv_timestamp
|
|
178
|
+
log_data["signature"] = signature
|
|
179
|
+
log_data["adv_flags"] = flags
|
|
180
|
+
log_data["adv_type"] = adv_type
|
|
181
|
+
if not adv_lat is None :
|
|
182
|
+
log_data["adv_lat"] = adv_lat
|
|
183
|
+
if not adv_lon is None :
|
|
184
|
+
log_data["adv_lon"] = adv_lon
|
|
185
|
+
if not adv_feat1 is None:
|
|
186
|
+
log_data["adv_feat1"] = adv_feat1
|
|
187
|
+
if not adv_feat2 is None:
|
|
188
|
+
log_data["adv_feat2"] = adv_feat2
|
|
189
|
+
|
|
190
|
+
return log_data
|
|
@@ -5,19 +5,15 @@ import time
|
|
|
5
5
|
import io
|
|
6
6
|
from typing import Any, Dict
|
|
7
7
|
from .events import Event, EventType, EventDispatcher, ErrorMessages
|
|
8
|
+
from .meshcore_parser import MeshcorePacketParser
|
|
8
9
|
from .packets import BinaryReqType, PacketType, ControlType
|
|
9
10
|
from .parsing import lpp_parse, lpp_parse_mma, parse_acl, parse_status
|
|
10
11
|
from cayennelpp import LppFrame, LppData
|
|
11
12
|
from meshcore.lpp_json_encoder import lpp_json_encoder
|
|
12
|
-
from Crypto.
|
|
13
|
-
from Crypto.Hash import HMAC, SHA256
|
|
13
|
+
from Crypto.Hash import SHA256
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger("meshcore")
|
|
16
16
|
|
|
17
|
-
PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
|
|
18
|
-
ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
|
|
19
|
-
CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
|
|
20
|
-
|
|
21
17
|
class MessageReader:
|
|
22
18
|
def __init__(self, dispatcher: EventDispatcher):
|
|
23
19
|
self.dispatcher = dispatcher
|
|
@@ -29,9 +25,15 @@ class MessageReader:
|
|
|
29
25
|
# Track pending binary requests by tag for proper response parsing
|
|
30
26
|
self.pending_binary_requests: Dict[str, Dict[str, Any]] = {} # tag -> {request_type, expires_at}
|
|
31
27
|
|
|
32
|
-
self.
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
self.packet_parser = MeshcorePacketParser()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def decrypt_channels(self):
|
|
32
|
+
return self.packet_parser.decrypt_channels
|
|
33
|
+
|
|
34
|
+
@decrypt_channels.setter
|
|
35
|
+
def decrypt_channels(self, value):
|
|
36
|
+
self.packet_parser.decrypt_channels = value
|
|
35
37
|
|
|
36
38
|
def register_binary_request(self, prefix: str, tag: str, request_type: BinaryReqType, timeout_seconds: float, context={}, is_anon=False):
|
|
37
39
|
"""Register a pending binary request for proper response parsing"""
|
|
@@ -266,7 +268,7 @@ class MessageReader:
|
|
|
266
268
|
# search for text in log_channels
|
|
267
269
|
txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
|
|
268
270
|
if self.decrypt_channels:
|
|
269
|
-
logged =
|
|
271
|
+
logged = await self.packet_parser.findLogChannelMsg(txt_hash)
|
|
270
272
|
if not logged is None:
|
|
271
273
|
res["path"] = logged["path"]
|
|
272
274
|
res["RSSI"] = logged["rssi"]
|
|
@@ -302,11 +304,11 @@ class MessageReader:
|
|
|
302
304
|
res["text"] = text.decode("utf-8", "ignore")
|
|
303
305
|
|
|
304
306
|
# search for text in log_channels
|
|
305
|
-
txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
|
|
306
|
-
res["txt_hash"] = txt_hash
|
|
307
|
-
logged = next((l for l in reversed(self.channels_log) if 'msg_hash' in l and l['msg_hash'] == txt_hash), None)
|
|
308
|
-
|
|
309
307
|
if self.decrypt_channels:
|
|
308
|
+
txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
|
|
309
|
+
res["txt_hash"] = txt_hash
|
|
310
|
+
logged = await self.packet_parser.findLogChannelMsg(txt_hash)
|
|
311
|
+
|
|
310
312
|
if not logged is None:
|
|
311
313
|
res["path"] = logged["path"]
|
|
312
314
|
res["RSSI"] = logged["rssi"]
|
|
@@ -476,9 +478,6 @@ class MessageReader:
|
|
|
476
478
|
idx = dbuf.read(1)[0]
|
|
477
479
|
res["channel_idx"] = idx
|
|
478
480
|
|
|
479
|
-
if len(self.channels) <= idx:
|
|
480
|
-
self.channels.extend([{} for _ in range(1 + idx - len(self.channels))])
|
|
481
|
-
|
|
482
481
|
# Channel name is null-terminated, so find the first null byte
|
|
483
482
|
name_bytes = dbuf.read(32)
|
|
484
483
|
null_pos = name_bytes.find(0)
|
|
@@ -490,7 +489,7 @@ class MessageReader:
|
|
|
490
489
|
res["channel_secret"] = dbuf.read(16)
|
|
491
490
|
res["channel_hash"] = SHA256.new(res["channel_secret"]).hexdigest()[0:2]
|
|
492
491
|
|
|
493
|
-
self.
|
|
492
|
+
await self.packet_parser.newChannel(res)
|
|
494
493
|
|
|
495
494
|
await self.dispatcher.dispatch(Event(EventType.CHANNEL_INFO, res, res))
|
|
496
495
|
|
|
@@ -607,157 +606,12 @@ class MessageReader:
|
|
|
607
606
|
log_data["payload"] = payload.hex()
|
|
608
607
|
log_data["payload_length"] = len(payload)
|
|
609
608
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
payload_type = (header & 0x3c) >> 2
|
|
617
|
-
payload_ver = (header & 0xc0) >> 6
|
|
618
|
-
|
|
619
|
-
transport_code = None
|
|
620
|
-
if route_type == 0x00 or route_type == 0x03: # has transport code
|
|
621
|
-
transport_code = pbuf.read(4) # discard transport code
|
|
622
|
-
|
|
623
|
-
path_byte = pbuf.read(1)[0]
|
|
624
|
-
path_hash_size = ((path_byte & 0xC0) >> 6) + 1
|
|
625
|
-
path_len = (path_byte & 0x3F)
|
|
626
|
-
# here path_len is number of hops, not number of bytes
|
|
627
|
-
|
|
628
|
-
path = pbuf.read(path_len*path_hash_size).hex() # Beware of traces where pathes are mixed
|
|
629
|
-
|
|
630
|
-
try :
|
|
631
|
-
route_typename = ROUTE_TYPENAMES[route_type]
|
|
632
|
-
except IndexError:
|
|
633
|
-
logger.debug(f"Unknown route type {route_type}")
|
|
634
|
-
route_typename = "UNK"
|
|
635
|
-
|
|
636
|
-
try :
|
|
637
|
-
payload_typename = PAYLOAD_TYPENAMES[payload_type]
|
|
638
|
-
except IndexError:
|
|
639
|
-
logger.debug(f"Unknown payload type {payload_type}")
|
|
640
|
-
payload_typename = "UNK"
|
|
641
|
-
|
|
642
|
-
pkt_payload = pbuf.read()
|
|
643
|
-
pkt_hash = int.from_bytes(SHA256.new(pkt_payload).digest()[0:4], "little", signed=False)
|
|
644
|
-
|
|
645
|
-
log_data["header"] = header
|
|
646
|
-
log_data["route_type"] = route_type
|
|
647
|
-
attributes["route_type"] = route_type
|
|
648
|
-
log_data["route_typename"] = route_typename
|
|
649
|
-
log_data["payload_type"] = payload_type
|
|
650
|
-
attributes["payload_type"] = payload_type
|
|
651
|
-
log_data["payload_typename"]= payload_typename
|
|
652
|
-
|
|
653
|
-
log_data["payload_ver"] = payload_ver
|
|
654
|
-
|
|
655
|
-
if not transport_code is None:
|
|
656
|
-
log_data["transport_code"] = transport_code.hex()
|
|
657
|
-
|
|
658
|
-
log_data["path_len"] = path_len
|
|
659
|
-
attributes["path_len"] = path_len
|
|
660
|
-
log_data["path_hash_size"] = path_hash_size
|
|
661
|
-
log_data["path"] = path
|
|
662
|
-
attributes["path"] = path
|
|
663
|
-
|
|
664
|
-
log_data["pkt_payload"] = pkt_payload
|
|
665
|
-
log_data["pkt_hash"] = pkt_hash
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if not payload is None and payload_type == 0x05: # flood msg / channel
|
|
669
|
-
pk_buf = io.BytesIO(pkt_payload)
|
|
670
|
-
chan_hash = pk_buf.read(1).hex()
|
|
671
|
-
cipher_mac = pk_buf.read(2)
|
|
672
|
-
msg = pk_buf.read() # until the end of buffer
|
|
673
|
-
|
|
674
|
-
channel = None
|
|
675
|
-
for c in self.channels:
|
|
676
|
-
if "channel_hash" in c and c["channel_hash"] == chan_hash : # validate against MAC
|
|
677
|
-
h = HMAC.new(c["channel_secret"], digestmod=SHA256)
|
|
678
|
-
h.update(msg)
|
|
679
|
-
if h.digest()[0:2] == cipher_mac:
|
|
680
|
-
channel = c
|
|
681
|
-
break
|
|
682
|
-
|
|
683
|
-
chan_name = ""
|
|
684
|
-
|
|
685
|
-
if channel is None :
|
|
686
|
-
chan_name = chan_hash
|
|
687
|
-
else:
|
|
688
|
-
chan_name = channel["channel_name"]
|
|
689
|
-
|
|
690
|
-
log_data["chan_hash"] = chan_hash
|
|
691
|
-
log_data["cipher_mac"] = cipher_mac.hex()
|
|
692
|
-
log_data["crypted"] = msg.hex()
|
|
693
|
-
log_data["chan_name"] = chan_name
|
|
694
|
-
|
|
695
|
-
if not channel is None and self.decrypt_channels:
|
|
696
|
-
# search for the same packet
|
|
697
|
-
logged = next((l for l in reversed(self.channels_log) if 'pkt_hash' in l and l['pkt_hash'] == pkt_hash), None)
|
|
698
|
-
|
|
699
|
-
if logged is None:
|
|
700
|
-
# not found: decrypt the text and hash it
|
|
701
|
-
aes_key = channel["channel_secret"]
|
|
702
|
-
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
703
|
-
uncrypted = cipher.decrypt(msg)
|
|
704
|
-
timestamp = int.from_bytes(uncrypted[0:4], "little", signed=False)
|
|
705
|
-
attempt = uncrypted[4] & 3
|
|
706
|
-
txt_type = int.from_bytes(uncrypted[4:4], "little", signed=False) >> 2
|
|
707
|
-
message = uncrypted[5:].strip(b"\0")
|
|
708
|
-
msg_hash = int.from_bytes(SHA256.new(timestamp.to_bytes(4, "little", signed=False) + message).digest()[0:4], "little", signed=False)
|
|
709
|
-
log_data["message"] = message.decode("utf-8", "ignore")
|
|
710
|
-
log_data["msg_hash"] = msg_hash
|
|
711
|
-
log_data["sender_timestamp"] = timestamp
|
|
712
|
-
log_data["attempt"] = attempt
|
|
713
|
-
log_data["txt_type"] = txt_type
|
|
714
|
-
else:
|
|
715
|
-
# found: copy
|
|
716
|
-
log_data["message"] = logged["message"]
|
|
717
|
-
log_data["msg_hash"] = logged["msg_hash"]
|
|
718
|
-
log_data["sender_timestamp"] = logged["sender_timestamp"]
|
|
719
|
-
log_data["attempt"] = logged["attempt"]
|
|
720
|
-
log_data["txt_type"] = logged["txt_type"]
|
|
721
|
-
|
|
722
|
-
self.channels_log.append(log_data)
|
|
723
|
-
if len(self.channels_log) > 100:
|
|
724
|
-
del self.channels_log[:25]
|
|
725
|
-
|
|
726
|
-
elif not payload is None and payload_type == 0x04: # Advert
|
|
727
|
-
pk_buf = io.BytesIO(pkt_payload)
|
|
728
|
-
adv_key = pk_buf.read(32).hex()
|
|
729
|
-
adv_timestamp = int.from_bytes(pk_buf.read(4), "little", signed=False)
|
|
730
|
-
signature = pk_buf.read(64).hex()
|
|
731
|
-
flags = pk_buf.read(1)[0]
|
|
732
|
-
adv_type = flags & 0x0F
|
|
733
|
-
adv_lat = None
|
|
734
|
-
adv_lon = None
|
|
735
|
-
adv_feat1 = None
|
|
736
|
-
adv_feat2 = None
|
|
737
|
-
if flags & 0x10 > 0: #has location
|
|
738
|
-
adv_lat = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
|
|
739
|
-
adv_lon = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
|
|
740
|
-
if flags & 0x20 > 0: #has feature1
|
|
741
|
-
adv_feat1 = pk_buf.read(2).hex()
|
|
742
|
-
if flags & 0x40 > 0: #has feature2
|
|
743
|
-
adv_feat2 = pk_buf.read(2).hex()
|
|
744
|
-
if flags & 0x80 > 0: #has name
|
|
745
|
-
adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
|
|
746
|
-
log_data["adv_name"] = adv_name
|
|
747
|
-
|
|
748
|
-
log_data["adv_key"] = adv_key
|
|
749
|
-
log_data["adv_timestamp"] = adv_timestamp
|
|
750
|
-
log_data["signature"] = signature
|
|
751
|
-
log_data["adv_flags"] = flags
|
|
752
|
-
log_data["adv_type"] = adv_type
|
|
753
|
-
if not adv_lat is None :
|
|
754
|
-
log_data["adv_lat"] = adv_lat
|
|
755
|
-
if not adv_lon is None :
|
|
756
|
-
log_data["adv_lon"] = adv_lon
|
|
757
|
-
if not adv_feat1 is None:
|
|
758
|
-
log_data["adv_feat1"] = adv_feat1
|
|
759
|
-
if not adv_feat2 is None:
|
|
760
|
-
log_data["adv_feat2"] = adv_feat2
|
|
609
|
+
# Parse payload and get some info from it
|
|
610
|
+
log_data = await self.packet_parser.parsePacketPayload(payload, log_data)
|
|
611
|
+
attributes['route_type'] = log_data['route_type']
|
|
612
|
+
attributes['payload_type'] = log_data['payload_type']
|
|
613
|
+
attributes['path_len'] = log_data['path_len']
|
|
614
|
+
attributes['path'] = log_data['path']
|
|
761
615
|
|
|
762
616
|
# Dispatch as RF log data
|
|
763
617
|
await self.dispatcher.dispatch(
|
|
@@ -973,13 +827,13 @@ class MessageReader:
|
|
|
973
827
|
res["pubkey_pre"] = dbuf.read(6).hex()
|
|
974
828
|
opl = dbuf.read(1)[0]
|
|
975
829
|
opl_hlen = ((opl & 0xc0) >> 6) + 1
|
|
976
|
-
opl = opl &
|
|
830
|
+
opl = opl & 0x3f
|
|
977
831
|
res["out_path_len"] = opl
|
|
978
832
|
res["out_path_hash_len"] = opl_hlen
|
|
979
833
|
res["out_path"] = dbuf.read(opl*opl_hlen).hex()
|
|
980
834
|
ipl = dbuf.read(1)[0]
|
|
981
835
|
ipl_hlen = ((ipl & 0xc0) >> 6) + 1
|
|
982
|
-
ipl = ipl &
|
|
836
|
+
ipl = ipl & 0x3f
|
|
983
837
|
res["in_path_len"] = ipl
|
|
984
838
|
res["in_path_hash_len"] = ipl_hlen
|
|
985
839
|
res["in_path"] = dbuf.read(ipl*ipl_hlen).hex()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from meshcore.events import EventType
|
|
4
|
+
from meshcore.reader import MessageReader
|
|
5
|
+
|
|
6
|
+
pytestmark = pytest.mark.asyncio
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockDispatcher:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.dispatched_events = []
|
|
12
|
+
|
|
13
|
+
async def dispatch(self, event):
|
|
14
|
+
self.dispatched_events.append(event)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def test_path_discovery_response_decodes_3byte_paths():
|
|
18
|
+
dispatcher = MockDispatcher()
|
|
19
|
+
reader = MessageReader(dispatcher)
|
|
20
|
+
|
|
21
|
+
packet = bytearray.fromhex(
|
|
22
|
+
"8d" # PacketType.PATH_DISCOVERY_RESPONSE
|
|
23
|
+
"00" # Reserved byte
|
|
24
|
+
"112233445566" # 6-byte contact public-key prefix
|
|
25
|
+
"82" # Outbound path descriptor: mode=2 (3-byte hops), hop_count=2
|
|
26
|
+
"a1a2a3b1b2b3" # Outbound path bytes: 2 hops * 3 bytes each
|
|
27
|
+
"81" # Inbound path descriptor: mode=2 (3-byte hops), hop_count=1
|
|
28
|
+
"c1c2c3" # Inbound path bytes: 1 hop * 3 bytes
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
await reader.handle_rx(packet)
|
|
32
|
+
|
|
33
|
+
matching = [e for e in dispatcher.dispatched_events if e.type == EventType.PATH_RESPONSE]
|
|
34
|
+
assert len(matching) == 1
|
|
35
|
+
|
|
36
|
+
payload = matching[0].payload
|
|
37
|
+
assert payload["pubkey_pre"] == "112233445566"
|
|
38
|
+
|
|
39
|
+
assert payload["out_path_hash_len"] == 3
|
|
40
|
+
assert payload["out_path_len"] == 2
|
|
41
|
+
assert payload["out_path"] == "a1a2a3b1b2b3"
|
|
42
|
+
|
|
43
|
+
assert payload["in_path_hash_len"] == 3
|
|
44
|
+
assert payload["in_path_len"] == 1
|
|
45
|
+
assert payload["in_path"] == "c1c2c3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|