qecho / src /services.py
valent-p's picture
feat: Implement secure data storage and retrieval services with encryption/decryption and add local mock tests for API endpoints.
92bdcb0
Raw History Blame Contribute Delete
4.93 kB
import logging
import sys
from uuid import uuid4
import base64
from fastapi import HTTPException
from cryptography.fernet import Fernet
from src.database import supabase
from src.security import generate_keys
from src.models import PayloadRequest, RetrieveGroupRequest
# --- LOGGING CONFIGURATION ---
logger = logging.getLogger("qecho")
logger.setLevel(logging.INFO)
# Avoid adding duplicate handlers if re-imported
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
# -----------------------------
async def process_store_data(payload: PayloadRequest):
if not payload.data:
raise HTTPException(status_code=400, detail="Data field is required")
logger.info(f"Attempting to store data for group: {payload.group_name}")
try:
# 1. Generate Keys
logger.info("Step 1: Generating Keys...")
enc_key, db_index = generate_keys(payload.group_name, payload.password)
logger.info(f"Keys generated. Blind Index hash: {db_index[:8]}...")
# 2. Encrypt
logger.info("Step 2: Encrypting payload...")
f = Fernet(enc_key)
encrypted_bytes = f.encrypt(payload.data.encode())
encrypted_string = base64.urlsafe_b64encode(encrypted_bytes).decode('utf-8')
record_id = str(uuid4())
logger.info(f"Payload encrypted. Record ID: {record_id}")
# 3. Save to Supabase using the Blind Index
logger.info("Step 3: Inserting into Supabase...")
data, count = supabase.table("secrets").insert({
"id": record_id,
"encrypted_data": encrypted_string,
"group_hash": db_index
}).execute()
logger.info("Supabase insert successful.")
return {"status": "success", "uuid": record_id, "message": "Securely stored."}
except Exception as e:
logger.exception("CRITICAL ERROR during store_data:")
# We can optionally expose the error detail to the user for debugging purposes
# since this is a dev/security tool, but be careful not to leak secrets.
raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}")
async def process_retrieve_data(payload: RetrieveGroupRequest):
logger.info(f"Attempting to retrieve data for group: {payload.group_name} with limit {payload.limit}")
try:
# 1. Generate Keys
logger.info("Step 1: Generating Keys...")
enc_key, db_index = generate_keys(payload.group_name, payload.password)
logger.info(f"Keys generated. Blind Index hash: {db_index[:8]}...")
# 2. Fetch
logger.info("Step 2: select from Supabase...")
response = supabase.table("secrets") \
.select("id, encrypted_data, created_at") \
.eq("group_hash", db_index) \
.order("created_at", desc=True) \
.limit(payload.limit) \
.execute()
if not response.data:
logger.info("No records found for this group.")
return {"messages": []}
logger.info(f"Found {len(response.data)} records. Attempting decryption...")
# 3. Decrypt
f = Fernet(enc_key)
decrypted_messages = []
ids_to_delete = [] # Keep track of what we successfully decrypted
for row in response.data:
try:
enc_bytes = base64.urlsafe_b64decode(row['encrypted_data'])
dec_msg = f.decrypt(enc_bytes).decode()
decrypted_messages.append({
"id": row['id'],
"timestamp": row['created_at'],
"content": dec_msg
})
# If decryption worked, mark for deletion
ids_to_delete.append(row['id'])
except Exception as decrypt_err:
logger.warning(f"Failed to decrypt message {row['id']}: {decrypt_err}")
continue
logger.info(f"Successfully decrypted {len(decrypted_messages)} messages.")
# 4. BURN AFTER READING (If requested)
if payload.burn and ids_to_delete:
logger.info(f"Burn requested. Deleting {len(ids_to_delete)} messages...")
supabase.table("secrets").delete().in_("id", ids_to_delete).execute()
logger.info("Burn complete.")
# Append a meta-message so the user knows
decrypted_messages.append({
"system_info": f"Securely burned {len(ids_to_delete)} messages from the server."
})
return {"messages": decrypted_messages}
except Exception as e:
logger.exception("CRITICAL ERROR during retrieve_data:")
raise HTTPException(status_code=500, detail=f"Retrieve failed: {str(e)}")