Add end-to-end encryption for chat messages (X25519 / NaCl box)
Each device generates its own X25519 key pair on login (tweetnacl); the private key never leaves localStorage or reaches our server. Messages are encrypted client-side with the recipient's public key before being sent, so the backend and anyone intercepting traffic only ever see ciphertext. - src/lib/e2ee.ts: key generation/registration + encrypt/decrypt (NaCl box) - Chat thread + matches list: encrypt on send, decrypt on receive/preview, with a graceful fallback when a message can't be decrypted on this device (e.g. after switching browsers) and full backward compat for legacy plaintext messages sent before this change - Privacy settings page explains the guarantee and its one deliberate trade-off: there is no key backup/escrow, so clearing browser data or switching devices makes old messages permanently unreadable Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+20
-3
@@ -51,6 +51,7 @@ export interface UserProfile {
|
||||
notify_matches: boolean;
|
||||
notify_messages: boolean;
|
||||
notify_likes: boolean;
|
||||
public_key?: string;
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
@@ -168,7 +169,11 @@ export interface MatchView {
|
||||
photos: string[];
|
||||
created_at: string;
|
||||
last_message?: string;
|
||||
last_message_cipher_text?: string;
|
||||
last_message_nonce?: string;
|
||||
last_at?: string;
|
||||
/** کلید عمومی X25519 طرف مقابل (base64)، برای رمزنگاری سرتاسری. اگه طرف مقابل هنوز کلید ثبت نکرده باشه خالیه. */
|
||||
partner_public_key?: string;
|
||||
}
|
||||
|
||||
export interface SwipeResult {
|
||||
@@ -180,7 +185,10 @@ export interface MessageView {
|
||||
id: number;
|
||||
match_id: number;
|
||||
sender_id: number;
|
||||
text: string;
|
||||
/** فقط برای پیامهای قدیمیِ قبل از رمزنگاری سرتاسری پر میشه */
|
||||
text?: string;
|
||||
cipher_text?: string;
|
||||
nonce?: string;
|
||||
created_at: string;
|
||||
is_mine: boolean;
|
||||
}
|
||||
@@ -494,9 +502,18 @@ export function listMessages(matchId: number) {
|
||||
return authRequest<MessageView[]>(`/matches/${matchId}/messages`);
|
||||
}
|
||||
|
||||
export function sendMessage(matchId: number, text: string) {
|
||||
// cipherText/nonce قبلاً روی دستگاه فرستنده با کلید عمومی طرف مقابل
|
||||
// رمزنگاری شدن (src/lib/e2ee.ts)؛ متن خام پیام هیچوقت به این تابع نمیرسه.
|
||||
export function sendMessage(matchId: number, cipherText: string, nonce: string) {
|
||||
return authRequest<MessageView>(`/matches/${matchId}/messages`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text }),
|
||||
body: JSON.stringify({ cipher_text: cipherText, nonce }),
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePublicKey(publicKey: string) {
|
||||
return authRequest<null>("/profile/public-key", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ public_key: publicKey }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user