mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Drop out channel hash helper
This commit is contained in:
@@ -85,15 +85,6 @@ class PacketInfo:
|
||||
path_hash_size: int = 1 # Bytes per hop: 1, 2, or 3
|
||||
|
||||
|
||||
def calculate_channel_hash(channel_key: bytes) -> str:
|
||||
"""
|
||||
Calculate the channel hash from a 16-byte channel key.
|
||||
Returns the first byte of SHA256(key) as hex.
|
||||
"""
|
||||
hash_bytes = hashlib.sha256(channel_key).digest()
|
||||
return format(hash_bytes[0], "02x")
|
||||
|
||||
|
||||
def extract_payload(raw_packet: bytes) -> bytes | None:
|
||||
"""
|
||||
Extract just the payload from a raw packet, skipping header and path.
|
||||
@@ -233,7 +224,7 @@ def try_decrypt_packet_with_channel_key(
|
||||
return None
|
||||
|
||||
packet_channel_hash = format(packet_info.payload[0], "02x")
|
||||
expected_hash = calculate_channel_hash(channel_key)
|
||||
expected_hash = format(hashlib.sha256(channel_key).digest()[0], "02x")
|
||||
|
||||
if packet_channel_hash != expected_hash:
|
||||
return None
|
||||
|
||||
@@ -857,6 +857,19 @@ class MessageRepository:
|
||||
"top_senders_24h": top_senders,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def count_channels_with_incoming_messages() -> int:
|
||||
"""Count distinct channel conversations with at least one incoming message."""
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT COUNT(DISTINCT conversation_key) AS cnt
|
||||
FROM messages
|
||||
WHERE type = 'CHAN' AND outgoing = 0
|
||||
"""
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return int(row["cnt"]) if row and row["cnt"] is not None else 0
|
||||
|
||||
@staticmethod
|
||||
async def get_most_active_rooms(sender_key: str, limit: int = 5) -> list[tuple[str, str, int]]:
|
||||
"""Get channels where a contact has sent the most messages.
|
||||
|
||||
@@ -13,6 +13,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.config import get_recent_log_lines, settings
|
||||
from app.radio_sync import get_contacts_selected_for_radio_sync, get_radio_channel_limit
|
||||
from app.repository import MessageRepository
|
||||
from app.routers.health import HealthResponse, build_health_data
|
||||
from app.services.radio_runtime import radio_runtime
|
||||
|
||||
@@ -34,6 +35,7 @@ class DebugRuntimeInfo(BaseModel):
|
||||
connection_desired: bool
|
||||
setup_in_progress: bool
|
||||
setup_complete: bool
|
||||
channels_with_incoming_messages: int
|
||||
max_channels: int
|
||||
path_hash_mode: int
|
||||
path_hash_mode_supported: bool
|
||||
@@ -269,6 +271,9 @@ async def debug_support_snapshot() -> DebugSnapshotResponse:
|
||||
"""Return a support/debug snapshot with recent logs and live radio state."""
|
||||
health_data = await build_health_data(radio_runtime.is_connected, radio_runtime.connection_info)
|
||||
radio_probe = await _probe_radio()
|
||||
channels_with_incoming_messages = (
|
||||
await MessageRepository.count_channels_with_incoming_messages()
|
||||
)
|
||||
return DebugSnapshotResponse(
|
||||
captured_at=datetime.now(timezone.utc).isoformat(),
|
||||
application=_build_application_info(),
|
||||
@@ -278,6 +283,7 @@ async def debug_support_snapshot() -> DebugSnapshotResponse:
|
||||
connection_desired=radio_runtime.connection_desired,
|
||||
setup_in_progress=radio_runtime.is_setup_in_progress,
|
||||
setup_complete=radio_runtime.is_setup_complete,
|
||||
channels_with_incoming_messages=channels_with_incoming_messages,
|
||||
max_channels=radio_runtime.max_channels,
|
||||
path_hash_mode=radio_runtime.path_hash_mode,
|
||||
path_hash_mode_supported=radio_runtime.path_hash_mode_supported,
|
||||
|
||||
@@ -138,7 +138,6 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message:
|
||||
require_connected()
|
||||
|
||||
# Get channel info from our database
|
||||
from app.decoder import calculate_channel_hash
|
||||
from app.repository import ChannelRepository
|
||||
|
||||
db_channel = await ChannelRepository.get_by_key(request.channel_key)
|
||||
@@ -155,13 +154,6 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message:
|
||||
status_code=400, detail=f"Invalid channel key format: {request.channel_key}"
|
||||
) from None
|
||||
|
||||
expected_hash = calculate_channel_hash(key_bytes)
|
||||
logger.info(
|
||||
"Sending to channel %s (%s) via managed radio slot, key hash: %s",
|
||||
request.channel_key,
|
||||
db_channel.name,
|
||||
expected_hash,
|
||||
)
|
||||
return await send_channel_message_to_channel(
|
||||
channel=db_channel,
|
||||
channel_key_upper=request.channel_key.upper(),
|
||||
|
||||
@@ -127,6 +127,13 @@ class TestDebugEndpoint:
|
||||
channel_key = "CD" * 16
|
||||
await _insert_contact(contact_key, "Alice", last_contacted=1700000000)
|
||||
await ChannelRepository.upsert(key=channel_key, name="#flightless", on_radio=False)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="Alice: hello",
|
||||
received_at=1700000001,
|
||||
conversation_key=channel_key,
|
||||
sender_timestamp=1700000001,
|
||||
)
|
||||
|
||||
radio_manager.max_channels = 2
|
||||
radio_manager.path_hash_mode = 1
|
||||
@@ -187,6 +194,7 @@ class TestDebugEndpoint:
|
||||
|
||||
assert payload["application"]["commit_hash"] == "deadbeef"
|
||||
assert payload["runtime"]["channel_slot_reuse_enabled"] is True
|
||||
assert payload["runtime"]["channels_with_incoming_messages"] == 1
|
||||
assert any("support snapshot marker" in line for line in payload["logs"])
|
||||
|
||||
radio_probe = payload["radio_probe"]
|
||||
@@ -226,6 +234,7 @@ class TestDebugEndpoint:
|
||||
payload = response.json()
|
||||
assert payload["radio_probe"]["performed"] is False
|
||||
assert payload["radio_probe"]["errors"] == ["Radio not connected"]
|
||||
assert payload["runtime"]["channels_with_incoming_messages"] == 0
|
||||
|
||||
|
||||
class TestRadioDisconnectedHandler:
|
||||
@@ -328,7 +337,6 @@ class TestMessagesEndpoint:
|
||||
radio_manager._meshcore = mock_mc
|
||||
with (
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
response = await client.post(
|
||||
|
||||
@@ -14,7 +14,6 @@ from app.decoder import (
|
||||
PayloadType,
|
||||
RouteType,
|
||||
_clamp_scalar,
|
||||
calculate_channel_hash,
|
||||
decrypt_direct_message,
|
||||
decrypt_group_text,
|
||||
derive_public_key,
|
||||
@@ -34,22 +33,8 @@ class TestChannelKeyDerivation:
|
||||
channel_name = "#test"
|
||||
expected_key = hashlib.sha256(channel_name.encode("utf-8")).digest()[:16]
|
||||
|
||||
# Verify the derived key produces the expected channel hash
|
||||
result_hash = calculate_channel_hash(expected_key)
|
||||
expected_hash = format(hashlib.sha256(expected_key).digest()[0], "02x")
|
||||
assert result_hash == expected_hash
|
||||
assert len(expected_key) == 16
|
||||
|
||||
def test_channel_hash_calculation(self):
|
||||
"""Channel hash is the first byte of SHA256(key) as hex."""
|
||||
key = bytes(16) # All zeros
|
||||
expected_hash = format(hashlib.sha256(key).digest()[0], "02x")
|
||||
|
||||
result = calculate_channel_hash(key)
|
||||
|
||||
assert result == expected_hash
|
||||
assert len(result) == 2 # Two hex chars
|
||||
|
||||
|
||||
class TestPacketParsing:
|
||||
"""Test raw packet header parsing."""
|
||||
|
||||
@@ -210,12 +210,10 @@ class TestChannelDecryption:
|
||||
|
||||
def test_channel_hash_matches_packet(self):
|
||||
"""Channel hash in packet matches hash computed from key."""
|
||||
from app.decoder import calculate_channel_hash
|
||||
|
||||
info = parse_packet(CHANNEL_PACKET)
|
||||
assert info is not None
|
||||
packet_hash = format(info.payload[0], "02x")
|
||||
expected_hash = calculate_channel_hash(CHANNEL_KEY)
|
||||
expected_hash = format(sha256(CHANNEL_KEY).digest()[0], "02x")
|
||||
assert packet_hash == expected_hash
|
||||
|
||||
def test_wrong_channel_key_fails(self):
|
||||
|
||||
@@ -209,7 +209,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event", side_effect=capture_broadcast),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=chan_key, text="!lasttime5 someone")
|
||||
@@ -234,7 +233,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=chan_key, text="acked now")
|
||||
@@ -262,7 +260,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event", side_effect=capture_broadcast),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=chan_key, text="hello")
|
||||
@@ -293,7 +290,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=chan_key, text="hello")
|
||||
@@ -317,7 +313,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=chan_key, text="hello")
|
||||
@@ -339,7 +334,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
@@ -362,7 +356,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
await send_channel_message(
|
||||
@@ -391,7 +384,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
await send_channel_message(
|
||||
@@ -433,7 +425,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
):
|
||||
await send_channel_message(
|
||||
@@ -458,7 +449,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
patch("app.radio.settings.force_channel_slot_reconfigure", True),
|
||||
):
|
||||
@@ -487,7 +477,6 @@ class TestOutgoingChannelBroadcast:
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user