// رمزنگاری سرتاسری (end-to-end) پیام‌های چت با NaCl box (X25519-XSalsa20-Poly1305). // کلید خصوصی هیچ‌وقت از این فایل بیرون نمی‌ره و هیچ‌وقت به سرور فرستاده نمی‌شه؛ // فقط توی localStorage همین مرورگر می‌مونه. یعنی اگه کاربر localStorage رو پاک // کنه یا از یه مرورگر/دستگاه دیگه وارد بشه، کلید قبلی از بین می‌ره و پیام‌های // قبلی دیگه قابل رمزگشایی نیستن — این یه تصمیم آگاهانه‌ست، نه باگ: هیچ نسخه‌ی // پشتیبان یا escrow از کلید خصوصی روی سرور نگه داشته نمی‌شه. import nacl from "tweetnacl"; import util from "tweetnacl-util"; import { updatePublicKey } from "./api"; const SECRET_KEY_STORAGE = "dating_app_e2ee_secret_key"; function isBrowser() { return typeof window !== "undefined"; } export interface LocalKeyPair { publicKey: Uint8Array; secretKey: Uint8Array; } function readLocalSecretKey(): Uint8Array | null { if (!isBrowser()) return null; const stored = localStorage.getItem(SECRET_KEY_STORAGE); if (!stored) return null; try { return util.decodeBase64(stored); } catch { return null; } } /** آیا این مرورگر از قبل کلید محلی داره؟ (بدون تولید کلید جدید) */ export function hasLocalKeyPair(): boolean { return readLocalSecretKey() !== null; } /** کلید محلی رو بدون هیچ درخواست شبکه‌ای برمی‌گردونه؛ اگه هنوز ساخته نشده null. */ export function getLocalKeyPair(): LocalKeyPair | null { const secretKey = readLocalSecretKey(); if (!secretKey) return null; return nacl.box.keyPair.fromSecretKey(secretKey); } /** * کلید محلی رو برمی‌گردونه؛ اگه وجود نداشته باشه یه جفت‌کلید تازه می‌سازه و * توی localStorage ذخیره می‌کنه. در هر دو حالت، کلید عمومی رو (دوباره) با * سرور sync می‌کنه تا اگه ثبت قبلی به هر دلیلی (قطعی شبکه و…) ناموفق بوده، * این بار درست بشه. */ export async function ensureKeyPair(): Promise { let secretKey = readLocalSecretKey(); let keyPair: nacl.BoxKeyPair; if (secretKey) { keyPair = nacl.box.keyPair.fromSecretKey(secretKey); } else { keyPair = nacl.box.keyPair(); secretKey = keyPair.secretKey; if (isBrowser()) { localStorage.setItem(SECRET_KEY_STORAGE, util.encodeBase64(secretKey)); } } try { await updatePublicKey(util.encodeBase64(keyPair.publicKey)); } catch { // آفلاین یا خطای موقت سرور؛ کلید محلی معتبره و دفعه‌ی بعد دوباره sync می‌شه. } return keyPair; } /** پیام متنی رو با کلید عمومی طرف مقابل رمزنگاری می‌کنه. */ export function encryptMessage( plainText: string, theirPublicKeyB64: string, mySecretKey: Uint8Array ): { cipherText: string; nonce: string } { const nonce = nacl.randomBytes(nacl.box.nonceLength); const messageBytes = util.decodeUTF8(plainText); const theirPublicKey = util.decodeBase64(theirPublicKeyB64); const box = nacl.box(messageBytes, nonce, theirPublicKey, mySecretKey); return { cipherText: util.encodeBase64(box), nonce: util.encodeBase64(nonce), }; } /** پیام رمزنگاری‌شده رو رمزگشایی می‌کنه؛ اگه کلید جور نباشه null برمی‌گردونه. */ export function decryptMessage( cipherTextB64: string, nonceB64: string, theirPublicKeyB64: string, mySecretKey: Uint8Array ): string | null { try { const cipherText = util.decodeBase64(cipherTextB64); const nonce = util.decodeBase64(nonceB64); const theirPublicKey = util.decodeBase64(theirPublicKeyB64); const opened = nacl.box.open(cipherText, nonce, theirPublicKey, mySecretKey); if (!opened) return null; return util.encodeUTF8(opened); } catch { return null; } }