cba4bc30da
git-subtree-dir: web git-subtree-mainline:75a0ba593dgit-subtree-split:8f624e7416
108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
// رمزنگاری سرتاسری (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;
|
|
}
|
|
}
|