diff --git a/backend/services/main/routes/envelope_messaging.py b/backend/services/main/routes/envelope_messaging.py index eb1b833..3efa2aa 100644 --- a/backend/services/main/routes/envelope_messaging.py +++ b/backend/services/main/routes/envelope_messaging.py @@ -14,7 +14,9 @@ import time from datetime import datetime from pathlib import Path from typing import Optional + import httpx +from nacl.exceptions import CryptoError from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from pydantic import BaseModel, Field @@ -376,6 +378,20 @@ async def send_encrypted_message( except HTTPException: raise + except CryptoError as e: + logger.warning( + "DM send: transport NaCl decrypt failed (message/file key mismatch or corrupt ciphertext): %s", + e, + ) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Transport decryption failed: the encrypted message and each attachment must be " + "encrypted with the same client ephemeral keypair. For resumable uploads, the " + "ciphertext bytes on the server must match the transport fields in this request—" + "re-encrypt on the client or abort the upload session and start over." + ), + ) from e except Exception as e: logger.exception("Error sending encrypted message: %s", e) raise HTTPException( diff --git a/backend/services/main/service_calls.py b/backend/services/main/service_calls.py index 70dedb1..408c9f9 100644 --- a/backend/services/main/service_calls.py +++ b/backend/services/main/service_calls.py @@ -10,6 +10,9 @@ import os import logging import json +import httpx +from fastapi import HTTPException, status + # Import request models for in-process calls logger = logging.getLogger("uvicorn.error") @@ -418,11 +421,23 @@ async def process_message_with_files_in_messaging_service( "files": transport_files, } try: - import httpx async with httpx.AsyncClient(timeout=timeout) as client: r = await client.post(url, json=payload) r.raise_for_status() return r.json() + except httpx.HTTPStatusError as e: + if e.response.status_code == status.HTTP_400_BAD_REQUEST: + try: + body = e.response.json() + detail = body.get("detail", str(body)) if isinstance(body, dict) else str(body) + except Exception: + detail = (e.response.text or "").strip() or str(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=detail, + ) from e + logger.error("Failed to process message+files in messaging service: %s", e) + raise except Exception as e: logger.error("Failed to process message+files in messaging service: %s", e) raise diff --git a/backend/services/messaging/main.py b/backend/services/messaging/main.py index d67c886..0988b7f 100644 --- a/backend/services/messaging/main.py +++ b/backend/services/messaging/main.py @@ -11,17 +11,21 @@ API Endpoints: """ import logging +import sys import time import base64 import os from typing import Dict, Any from fastapi import FastAPI, HTTPException, status +from nacl.exceptions import CryptoError from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager from pydantic import BaseModel logger = logging.getLogger("uvicorn.error") +_B64_DECODE_KW = {"validate": True} if sys.version_info >= (3, 11) else {} + # Import encryption modules from .encryption import generate_nonce, TRANSPORT_NONCE_SIZE, decrypt_transport_blob, decrypt_transport_message from .processor import process_encrypted_message, process_encrypted_message_and_files @@ -371,16 +375,29 @@ async def process_message_with_files( plaintext_files: list[bytes] = [] filenames: list[str] = [] - for tf in transport_files: + for idx, tf in enumerate(transport_files): enc_b64 = tf.get("encrypted_file_data_b64", "") - transport_blob = base64.b64decode(enc_b64) - plaintext_files.append( - decrypt_transport_blob( - client_public_key_b64=client_public_key_b64, - encrypted_blob=transport_blob, - ephemeral_private_key=private_key, + try: + transport_blob = base64.b64decode(enc_b64, **_B64_DECODE_KW) + except Exception as e: + logger.error("Invalid base64 for transport file index=%s filename=%r: %s", idx, tf.get("filename"), e) + raise + try: + plaintext_files.append( + decrypt_transport_blob( + client_public_key_b64=client_public_key_b64, + encrypted_blob=transport_blob, + ephemeral_private_key=private_key, + ) ) - ) + except Exception as e: + logger.error( + "Transport file decrypt failed index=%s filename=%r (check same ephemeral as message): %s", + idx, + tf.get("filename"), + e, + ) + raise filenames.append(tf.get("filename", "file")) return process_encrypted_message_and_files( @@ -417,6 +434,15 @@ async def process_message_with_files_http(request: ProcessMessageWithFilesReques recipient_public_key_b64=request.recipient_public_key_b64, transport_files=transport_files, ) + except CryptoError as e: + logger.warning("process-with-files: transport CryptoError: %s", e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Transport decryption failed: message and files must use the same client ephemeral " + "key as when file ciphertext was produced." + ), + ) from e except Exception as e: logger.exception("Failed to process message with files: %s", e) raise