chore: merge web repo history into monorepo at web/

git-subtree-dir: web
git-subtree-mainline: 75a0ba593d
git-subtree-split: 8f624e7416
This commit is contained in:
2026-09-27 10:31:24 +03:30
76 changed files with 13269 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
// رمزنگاری سرتاسری (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<LocalKeyPair> {
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;
}
}