from __future__ import annotations import base64 import json import os import hashlib from dataclasses import dataclass from pathlib import Path from cryptography.hazmat.primitives.ciphers.aead import AESGCM ROOT = Path(__file__).resolve().parents[1] LINK_PATH = ROOT / "data" / "phone_link.json" @dataclass(frozen=True, slots=True) class LinkConfig: # Either a raw key (legacy) or a human passphrase (preferred for phone pairing). key_b64: str = "" passphrase: str = "" device_name: str = "phone" # Optional: allowlist of Telegram user id(s) as strings. telegram_allow: list[str] | None = None def key_bytes(self) -> bytes: if self.passphrase: return hashlib.sha256(self.passphrase.encode("utf-8")).digest() return base64.b64decode(self.key_b64.encode("utf-8")) def load_or_create_link() -> LinkConfig: LINK_PATH.parent.mkdir(parents=True, exist_ok=True) if not LINK_PATH.exists(): data = {} else: try: data = json.loads(LINK_PATH.read_text(encoding="utf-8")) except Exception: data = {} # Ensure passphrase exists (preferred pairing method). Migrate older installs. if not str(data.get("passphrase", "")).strip(): data["passphrase"] = base64.b32encode(os.urandom(10)).decode("utf-8").rstrip("=").lower() # Keep legacy key_b64 if present; otherwise empty. data.setdefault("key_b64", "") data.setdefault("device_name", "phone") data.setdefault("telegram_allow", []) try: LINK_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") except Exception: pass return LinkConfig( key_b64=str(data.get("key_b64", "")), passphrase=str(data.get("passphrase", "")), device_name=str(data.get("device_name", "phone")), telegram_allow=list(data.get("telegram_allow", []) or []), ) def encrypt_json(obj: dict, key: bytes) -> dict: aes = AESGCM(key) nonce = os.urandom(12) pt = json.dumps(obj, separators=(",", ":"), ensure_ascii=True).encode("utf-8") ct = aes.encrypt(nonce, pt, None) return {"nonce": base64.b64encode(nonce).decode("utf-8"), "ciphertext": base64.b64encode(ct).decode("utf-8")} def decrypt_json(payload: dict, key: bytes) -> dict: aes = AESGCM(key) nonce = base64.b64decode(str(payload.get("nonce", "")).encode("utf-8")) ct = base64.b64decode(str(payload.get("ciphertext", "")).encode("utf-8")) pt = aes.decrypt(nonce, ct, None) return json.loads(pt.decode("utf-8"))