diff --git a/app/fanout/apprise_mod.py b/app/fanout/apprise_mod.py index 0cbf31d..1453d85 100644 --- a/app/fanout/apprise_mod.py +++ b/app/fanout/apprise_mod.py @@ -7,6 +7,7 @@ import logging from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from app.fanout.base import FanoutModule +from app.path_utils import split_path_hex logger = logging.getLogger(__name__) @@ -45,9 +46,12 @@ def _format_body(data: dict, *, include_path: bool) -> str: if include_path: paths = data.get("paths") if paths and isinstance(paths, list) and len(paths) > 0: - path_str = paths[0].get("path", "") if isinstance(paths[0], dict) else "" + first_path = paths[0] if isinstance(paths[0], dict) else {} + path_str = first_path.get("path", "") + path_len = first_path.get("path_len") else: path_str = None + path_len = None if msg_type == "PRIV" and path_str is None: via = " **via:** [`direct`]" @@ -56,7 +60,8 @@ def _format_body(data: dict, *, include_path: bool) -> str: if path_str == "": via = " **via:** [`direct`]" else: - hops = [path_str[i : i + 2] for i in range(0, len(path_str), 2)] + hop_count = path_len if isinstance(path_len, int) else len(path_str) // 2 + hops = split_path_hex(path_str, hop_count) if hops: hop_list = ", ".join(f"`{h}`" for h in hops) via = f" **via:** [{hop_list}]" diff --git a/app/fanout/community_mqtt.py b/app/fanout/community_mqtt.py index aafee63..48799f7 100644 --- a/app/fanout/community_mqtt.py +++ b/app/fanout/community_mqtt.py @@ -24,6 +24,7 @@ import aiomqtt import nacl.bindings from app.fanout.mqtt_base import BaseMqttPublisher +from app.path_utils import split_path_hex logger = logging.getLogger(__name__) @@ -153,23 +154,29 @@ def _calculate_packet_hash(raw_bytes: bytes) -> str: if has_transport: offset += 4 # Skip 4 bytes of transport codes - # Read path_len (1 byte on wire). Invalid/truncated packets map to zero hash. + # Read path byte (packed as [hash_mode:2][hop_count:6]). + # Invalid/truncated packets map to zero hash. if offset >= len(raw_bytes): return "0" * 16 - path_len = raw_bytes[offset] + path_byte = raw_bytes[offset] offset += 1 + hash_mode = (path_byte >> 6) & 0x03 + hop_count = path_byte & 0x3F + hash_size = (hash_mode + 1) if hash_mode < 3 else 1 + path_wire_len = hop_count * hash_size # Skip past path to get to payload. Invalid/truncated packets map to zero hash. - if len(raw_bytes) < offset + path_len: + if len(raw_bytes) < offset + path_wire_len: return "0" * 16 - payload_start = offset + path_len + payload_start = offset + path_wire_len payload_data = raw_bytes[payload_start:] - # Hash: payload_type(1 byte) [+ path_len as uint16_t LE for TRACE] + payload_data + # Hash: payload_type(1 byte) [+ path_byte as uint16_t LE for TRACE] + payload_data + # IMPORTANT: TRACE hash uses the raw wire byte (not decoded hop count) to match firmware. hash_obj = hashlib.sha256() hash_obj.update(bytes([payload_type])) if payload_type == 9: # PAYLOAD_TYPE_TRACE - hash_obj.update(path_len.to_bytes(2, byteorder="little")) + hash_obj.update(path_byte.to_bytes(2, byteorder="little")) hash_obj.update(payload_data) return hash_obj.hexdigest()[:16].upper() @@ -209,20 +216,24 @@ def _decode_packet_fields(raw_bytes: bytes) -> tuple[str, str, str, list[str], i if len(raw_bytes) <= offset: return route, packet_type, payload_len, path_values, payload_type - path_len = raw_bytes[offset] + path_byte = raw_bytes[offset] offset += 1 + hash_mode = (path_byte >> 6) & 0x03 + hop_count = path_byte & 0x3F + hash_size = (hash_mode + 1) if hash_mode < 3 else 1 + path_wire_len = hop_count * hash_size - if len(raw_bytes) < offset + path_len: + if len(raw_bytes) < offset + path_wire_len: return route, packet_type, payload_len, path_values, payload_type - path_bytes = raw_bytes[offset : offset + path_len] - offset += path_len + path_bytes = raw_bytes[offset : offset + path_wire_len] + offset += path_wire_len payload_type = (header >> 2) & 0x0F route = _ROUTE_MAP.get(route_type, "U") packet_type = str(payload_type) payload_len = str(max(0, len(raw_bytes) - offset)) - path_values = [f"{b:02x}" for b in path_bytes] + path_values = split_path_hex(path_bytes.hex(), hop_count) return route, packet_type, payload_len, path_values, payload_type except Exception: diff --git a/app/migrations.py b/app/migrations.py index bc5d869..53e4498 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -456,16 +456,20 @@ def _extract_payload_for_hash(raw_packet: bytes) -> bytes | None: return None offset += 4 - # Get path length + # Get path byte (packed as [hash_mode:2][hop_count:6]) if len(raw_packet) < offset + 1: return None - path_length = raw_packet[offset] + path_byte = raw_packet[offset] offset += 1 + hash_mode = (path_byte >> 6) & 0x03 + hop_count = path_byte & 0x3F + hash_size = (hash_mode + 1) if hash_mode < 3 else 1 + path_wire_len = hop_count * hash_size # Skip path bytes - if len(raw_packet) < offset + path_length: + if len(raw_packet) < offset + path_wire_len: return None - offset += path_length + offset += path_wire_len # Rest is payload (may be empty, matching decoder.py behavior) return raw_packet[offset:] @@ -638,16 +642,20 @@ def _extract_path_from_packet(raw_packet: bytes) -> str | None: return None offset += 4 - # Get path length + # Get path byte (packed as [hash_mode:2][hop_count:6]) if len(raw_packet) < offset + 1: return None - path_length = raw_packet[offset] + path_byte = raw_packet[offset] offset += 1 + hash_mode = (path_byte >> 6) & 0x03 + hop_count = path_byte & 0x3F + hash_size = (hash_mode + 1) if hash_mode < 3 else 1 + path_wire_len = hop_count * hash_size # Extract path bytes - if len(raw_packet) < offset + path_length: + if len(raw_packet) < offset + path_wire_len: return None - path_bytes = raw_packet[offset : offset + path_length] + path_bytes = raw_packet[offset : offset + path_wire_len] return path_bytes.hex() except (IndexError, ValueError): diff --git a/frontend/src/components/CrackerPanel.tsx b/frontend/src/components/CrackerPanel.tsx index 73e0110..a7f6793 100644 --- a/frontend/src/components/CrackerPanel.tsx +++ b/frontend/src/components/CrackerPanel.tsx @@ -24,15 +24,18 @@ function extractPayload(packetHex: string): string | null { offset += 8; // 4 bytes = 8 hex chars } - // Get path length + // Get path byte (packed as [hash_mode:2][hop_count:6]) if (packetHex.length < offset + 2) return null; - const pathLength = parseInt(packetHex.slice(offset, offset + 2), 16); + const pathByte = parseInt(packetHex.slice(offset, offset + 2), 16); offset += 2; + const hashMode = (pathByte >> 6) & 0x03; + const hopCount = pathByte & 0x3f; + const hashSize = hashMode < 3 ? hashMode + 1 : 1; + const pathHexChars = hopCount * hashSize * 2; // Skip path data - const pathBytes = pathLength * 2; // hex chars - if (packetHex.length < offset + pathBytes) return null; - offset += pathBytes; + if (packetHex.length < offset + pathHexChars) return null; + offset += pathHexChars; // Rest is payload return packetHex.slice(offset);