mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-12 03:33:03 +02:00
Add historical DM decryption
This commit is contained in:
+165
-4
@@ -5,8 +5,13 @@ from fastapi import APIRouter, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.database import db
|
||||
from app.decoder import parse_packet, try_decrypt_packet_with_channel_key
|
||||
from app.packet_processor import create_message_from_decrypted
|
||||
from app.decoder import (
|
||||
derive_public_key,
|
||||
parse_packet,
|
||||
try_decrypt_dm,
|
||||
try_decrypt_packet_with_channel_key,
|
||||
)
|
||||
from app.packet_processor import create_dm_message_from_decrypted, create_message_from_decrypted
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,6 +26,14 @@ class DecryptRequest(BaseModel):
|
||||
channel_name: str | None = Field(
|
||||
default=None, description="Channel name (for hashtag channels, key derived from name)"
|
||||
)
|
||||
# Fields for contact (DM) decryption
|
||||
private_key: str | None = Field(
|
||||
default=None,
|
||||
description="Our private key as hex (64 bytes = 128 chars, Ed25519 seed + pubkey)",
|
||||
)
|
||||
contact_public_key: str | None = Field(
|
||||
default=None, description="Contact's public key as hex (32 bytes = 64 chars)"
|
||||
)
|
||||
|
||||
|
||||
class DecryptResult(BaseModel):
|
||||
@@ -94,6 +107,84 @@ async def _run_historical_decryption(channel_key_bytes: bytes, channel_key_hex:
|
||||
logger.info("Historical decryption complete: %d/%d packets decrypted", decrypted_count, total)
|
||||
|
||||
|
||||
async def _run_historical_dm_decryption(
|
||||
private_key_bytes: bytes,
|
||||
contact_public_key_bytes: bytes,
|
||||
contact_public_key_hex: str,
|
||||
) -> None:
|
||||
"""Background task to decrypt historical DM packets with contact's key."""
|
||||
global _decrypt_progress
|
||||
|
||||
# Get only TEXT_MESSAGE packets (undecrypted)
|
||||
packets = await RawPacketRepository.get_undecrypted_text_messages()
|
||||
total = len(packets)
|
||||
processed = 0
|
||||
decrypted_count = 0
|
||||
|
||||
_decrypt_progress = DecryptProgress(total=total, processed=0, decrypted=0, in_progress=True)
|
||||
|
||||
logger.info("Starting historical DM decryption of %d TEXT_MESSAGE packets", total)
|
||||
|
||||
# Derive our public key from the private key using Ed25519 scalar multiplication.
|
||||
# Note: MeshCore stores the scalar directly (not a seed), so we use noclamp variant.
|
||||
# See derive_public_key() for details on the MeshCore key format.
|
||||
our_public_key_bytes = derive_public_key(private_key_bytes)
|
||||
|
||||
for packet_id, packet_data, packet_timestamp in packets:
|
||||
# Don't pass our_public_key - we want to decrypt both incoming AND outgoing messages.
|
||||
# The our_public_key filter in try_decrypt_dm only matches incoming (dest_hash == us),
|
||||
# which would skip outgoing messages (where dest_hash == contact).
|
||||
result = try_decrypt_dm(
|
||||
packet_data,
|
||||
private_key_bytes,
|
||||
contact_public_key_bytes,
|
||||
our_public_key=None,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
# Successfully decrypted - determine if inbound or outbound by checking src_hash
|
||||
src_hash = result.src_hash.lower()
|
||||
our_first_byte = format(our_public_key_bytes[0], "02x").lower()
|
||||
outgoing = src_hash == our_first_byte
|
||||
|
||||
logger.debug(
|
||||
"Decrypted DM packet %d: message=%s (outgoing=%s)",
|
||||
packet_id,
|
||||
result.message[:50] if result.message else "",
|
||||
outgoing,
|
||||
)
|
||||
|
||||
# Extract path from the raw packet for storage
|
||||
packet_info = parse_packet(packet_data)
|
||||
path_hex = packet_info.path.hex() if packet_info else None
|
||||
|
||||
msg_id = await create_dm_message_from_decrypted(
|
||||
packet_id=packet_id,
|
||||
decrypted=result,
|
||||
their_public_key=contact_public_key_hex,
|
||||
our_public_key=our_public_key_bytes.hex(),
|
||||
received_at=packet_timestamp,
|
||||
path=path_hex,
|
||||
outgoing=outgoing,
|
||||
)
|
||||
|
||||
if msg_id is not None:
|
||||
decrypted_count += 1
|
||||
|
||||
processed += 1
|
||||
_decrypt_progress = DecryptProgress(
|
||||
total=total, processed=processed, decrypted=decrypted_count, in_progress=True
|
||||
)
|
||||
|
||||
_decrypt_progress = DecryptProgress(
|
||||
total=total, processed=processed, decrypted=decrypted_count, in_progress=False
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Historical DM decryption complete: %d/%d packets decrypted", decrypted_count, total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/undecrypted/count")
|
||||
async def get_undecrypted_count() -> dict:
|
||||
"""Get the count of undecrypted packets."""
|
||||
@@ -151,12 +242,82 @@ async def decrypt_historical_packets(
|
||||
total_packets=0,
|
||||
message="Must provide channel_key or channel_name",
|
||||
)
|
||||
elif request.key_type == "contact":
|
||||
# Validate required fields for contact decryption
|
||||
if not request.private_key:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Must provide private_key for contact decryption",
|
||||
)
|
||||
if not request.contact_public_key:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Must provide contact_public_key for contact decryption",
|
||||
)
|
||||
|
||||
# Parse private key
|
||||
try:
|
||||
private_key_bytes = bytes.fromhex(request.private_key)
|
||||
if len(private_key_bytes) != 64:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Private key must be 64 bytes (128 hex chars)",
|
||||
)
|
||||
except ValueError:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Invalid hex string for private key",
|
||||
)
|
||||
|
||||
# Parse contact public key
|
||||
try:
|
||||
contact_public_key_bytes = bytes.fromhex(request.contact_public_key)
|
||||
if len(contact_public_key_bytes) != 32:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Contact public key must be 32 bytes (64 hex chars)",
|
||||
)
|
||||
contact_public_key_hex = request.contact_public_key.lower()
|
||||
except ValueError:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Invalid hex string for contact public key",
|
||||
)
|
||||
|
||||
# Get count of undecrypted TEXT_MESSAGE packets
|
||||
packets = await RawPacketRepository.get_undecrypted_text_messages()
|
||||
count = len(packets)
|
||||
if count == 0:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="No undecrypted TEXT_MESSAGE packets to process",
|
||||
)
|
||||
|
||||
# Start background decryption
|
||||
background_tasks.add_task(
|
||||
_run_historical_dm_decryption,
|
||||
private_key_bytes,
|
||||
contact_public_key_bytes,
|
||||
contact_public_key_hex,
|
||||
)
|
||||
|
||||
return DecryptResult(
|
||||
started=True,
|
||||
total_packets=count,
|
||||
message=f"Started DM decryption of {count} TEXT_MESSAGE packets in background",
|
||||
)
|
||||
else:
|
||||
# Contact decryption not yet supported (requires Ed25519 shared secret)
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Contact key decryption not yet supported",
|
||||
message="key_type must be 'channel' or 'contact'",
|
||||
)
|
||||
|
||||
# Get count of undecrypted packets
|
||||
|
||||
Reference in New Issue
Block a user