112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
if TYPE_CHECKING:
|
|
from nio import AsyncClient
|
|
|
|
_ENCRYPTION_DOC_URL = "https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide"
|
|
|
|
|
|
def resolve_encryption_config_path(base_dir: str, homeserver: str) -> str:
|
|
sanitized = homeserver.replace("://", "_").replace("/", "_")
|
|
return os.path.join(base_dir, "matrix_crypto_store", sanitized)
|
|
|
|
|
|
def format_encryption_unavailable_error(config: dict[str, Any]) -> str:
|
|
homeserver = config.get("homeserver", "unknown")
|
|
crypto_dir = config.get("crypto_store_dir", "")
|
|
|
|
parts = [
|
|
f"Encrypted messages received but E2EE is not configured for {homeserver}.",
|
|
]
|
|
|
|
if not crypto_dir:
|
|
parts.append("Set 'crypto_store_dir' to enable E2EE session key storage.")
|
|
else:
|
|
parts.append(f"Current crypto_store_dir: '{crypto_dir}' — verify it exists and is writable.")
|
|
|
|
parts.append(f"See: {_ENCRYPTION_DOC_URL}")
|
|
return "\n".join(parts)
|
|
|
|
|
|
def check_crypto_store_ready(store_path: str) -> dict[str, Any]:
|
|
status = "not_configured"
|
|
details: list[str] = []
|
|
|
|
if not store_path:
|
|
return {"status": status, "details": ["crypto_store_dir not configured"]}
|
|
|
|
if not os.path.exists(store_path):
|
|
details.append(f"Directory does not exist: {store_path}")
|
|
return {"status": "unavailable", "details": details}
|
|
|
|
if not os.path.isdir(store_path):
|
|
details.append(f"Path is not a directory: {store_path}")
|
|
return {"status": "unavailable", "details": details}
|
|
|
|
if not os.access(store_path, os.W_OK):
|
|
details.append(f"Directory not writable: {store_path}")
|
|
return {"status": "unavailable", "details": details}
|
|
|
|
keys_file = os.path.join(store_path, "room_keys.json")
|
|
if os.path.exists(keys_file):
|
|
status = "ready"
|
|
details.append("room_keys.json found")
|
|
else:
|
|
status = "empty"
|
|
details.append("room_keys.json not found — keys will be created on first encrypted message")
|
|
|
|
return {"status": status, "details": details}
|
|
|
|
|
|
_CRYPTO_BOOTSTRAP_GUIDE = """
|
|
E2EE Bootstrap Steps:
|
|
1. Ensure the crypto store directory exists and is writable
|
|
2. Import room keys (automatic on connect via import_room_keys)
|
|
3. Verify device list is tracked (automatic via nio AsyncClient)
|
|
4. If keys are missing, re-verify with: /keys/query endpoint
|
|
""".strip()
|
|
|
|
|
|
def get_crypto_bootstrap_guide() -> str:
|
|
return _CRYPTO_BOOTSTRAP_GUIDE
|
|
|
|
|
|
async def decrypt_event(client: AsyncClient, event: Any, room_id: str) -> dict[str, Any] | None:
|
|
try:
|
|
decrypted_list = await client.decrypt_event(event)
|
|
if decrypted_list and isinstance(decrypted_list, list):
|
|
for item in decrypted_list:
|
|
if isinstance(item, dict):
|
|
return item
|
|
if isinstance(decrypted_list, dict):
|
|
return decrypted_list
|
|
except Exception as e:
|
|
logger.debug(f"Matrix decrypt_event failed for {room_id}: {e}")
|
|
return None
|
|
|
|
|
|
async def verify_device(client: AsyncClient, user_id: str, device_id: str) -> dict[str, Any]:
|
|
try:
|
|
resp = await client.verify_device(user_id, device_id)
|
|
return {"status": "ok", "verified": bool(resp)}
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
async def check_cross_signing(client: AsyncClient) -> dict[str, Any]:
|
|
try:
|
|
resp = await client.query_keys()
|
|
cross_signing = getattr(resp, "cross_signing_keys", {}) if resp else {}
|
|
return {
|
|
"status": "ok",
|
|
"cross_signing_ready": bool(cross_signing),
|
|
"details": str(cross_signing)[:200] if cross_signing else "none",
|
|
}
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|