From f98b91ea3ca5ad41a2e55bd1172be68c95b146d9 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Wed, 24 Sep 2025 15:18:30 +0300 Subject: [PATCH] Improve code splitting --- frontend/src/api/dmApi.ts | 49 ++------------------- frontend/src/auth/crypto.ts | 4 +- frontend/src/ui/components/chat/Message.tsx | 9 ++-- frontend/src/utils/crypto/asymmetric.ts | 9 ++-- 4 files changed, 17 insertions(+), 54 deletions(-) diff --git a/frontend/src/api/dmApi.ts b/frontend/src/api/dmApi.ts index 415de5a..479e4e4 100644 --- a/frontend/src/api/dmApi.ts +++ b/frontend/src/api/dmApi.ts @@ -5,56 +5,15 @@ import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/s import { randomBytes } from "../utils/crypto/kdf"; import { getCurrentKeys } from "../auth/crypto"; import { request } from "../core/websocket"; -import type { FetchDMResponse, SendDMRequest, DmEnvelope, User } from "../core/types"; +import type { SendDMRequest, DmEnvelope, User } from "../core/types"; import { b64, ub64 } from "../utils/utils"; -export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string, token: string): Promise { - const keys = getCurrentKeys(); - if (!keys) throw new Error("Keys not initialized"); - const mk = randomBytes(32); - const wkSalt = randomBytes(16); - const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64)); - const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); - const wk = await importAesGcmKey(wkRaw); - const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext)); - const wrap = await aesGcmEncrypt(wk, mk); - - await fetch(`${API_BASE_URL}/dm/send`, { - method: "POST", - headers: getAuthHeaders(token, true), - body: JSON.stringify({ - recipientId: recipientId, - iv: b64(encMsg.iv), - ciphertext: b64(encMsg.ciphertext), - salt: b64(wkSalt), - iv2: b64(wrap.iv), - wrappedMk: b64(wrap.ciphertext) - }) - }); -} - -export async function fetchDm(since: number | undefined, token: string): Promise { - const url = new URL(`${API_BASE_URL}/dm/fetch`); - if (since) url.searchParams.set("since", String(since)); - - const response = await fetch(url, { - headers: getAuthHeaders(token, true) - }); - - if (response.ok) { - const data: FetchDMResponse = await response.json(); - return data.messages ?? []; - } else { - return []; - } -} - export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { const keys = getCurrentKeys(); if (!keys) throw new Error("Keys not initialized"); // Obtain the key - const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64)); + const shared = await ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64)); const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1])); const wk = await importAesGcmKey(wkRaw); const mk = await aesGcmDecrypt(wk, ub64(envelope.iv2), ub64(envelope.wrappedMk)); @@ -94,7 +53,7 @@ export async function sendDMViaWebSocket(recipientId: number, recipientPublicKey // Encryption key const mk = randomBytes(32); const wkSalt = randomBytes(16); - const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64)); + const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64)); const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); const wk = await importAesGcmKey(wkRaw); @@ -119,4 +78,4 @@ export async function sendDMViaWebSocket(recipientId: number, recipientPublicKey }, data: payload }); -} +} \ No newline at end of file diff --git a/frontend/src/auth/crypto.ts b/frontend/src/auth/crypto.ts index 1e16ff1..7e1eed9 100644 --- a/frontend/src/auth/crypto.ts +++ b/frontend/src/auth/crypto.ts @@ -90,7 +90,7 @@ export async function ensureKeysOnLogin(password: string, token: string): Promis currentPublicKey = serverPub; } else { // We don't have the corresponding public key from server; regenerate pair to resync - const pair = generateX25519KeyPair(); + const pair = await generateX25519KeyPair(); currentPublicKey = pair.publicKey; currentPrivateKey = pair.privateKey; await uploadPublicKey(currentPublicKey, token); @@ -107,7 +107,7 @@ export async function ensureKeysOnLogin(password: string, token: string): Promis } // First-time setup: generate keys and upload - const pair = generateX25519KeyPair(); + const pair = await generateX25519KeyPair(); currentPublicKey = pair.publicKey; currentPrivateKey = pair.privateKey; await uploadPublicKey(currentPublicKey, token); diff --git a/frontend/src/ui/components/chat/Message.tsx b/frontend/src/ui/components/chat/Message.tsx index e5835be..8b42a22 100644 --- a/frontend/src/ui/components/chat/Message.tsx +++ b/frontend/src/ui/components/chat/Message.tsx @@ -3,7 +3,6 @@ import type { Message as MessageType } from "../../../core/types"; import defaultAvatar from "../../../resources/images/default-avatar.png"; import Quote from "../core/Quote"; import { parse } from "marked"; -import DOMPurify from "dompurify"; import { useEffect, useState } from "react"; interface MessageProps { @@ -16,13 +15,15 @@ interface MessageProps { } export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false, isDm = false }: MessageProps) { - const [formattedMessage, setFormattedMessage] = useState({ __html: DOMPurify.sanitize(message.content).trim() }); + const [formattedMessage, setFormattedMessage] = useState({ __html: "" }); useEffect(() => { (async () => { + const DOMPurify = await import("dompurify"); + setFormattedMessage({ - __html: DOMPurify.sanitize( - await parse(message.content) + __html: DOMPurify.default.sanitize( + await parse(message.content.trim()) ).trim() }); })(); diff --git a/frontend/src/utils/crypto/asymmetric.ts b/frontend/src/utils/crypto/asymmetric.ts index 09b8dfa..1d747f2 100644 --- a/frontend/src/utils/crypto/asymmetric.ts +++ b/frontend/src/utils/crypto/asymmetric.ts @@ -1,4 +1,3 @@ -import nacl from "tweetnacl"; import { hkdfExtractAndExpand } from "../crypto/kdf"; export interface X25519KeyPair { @@ -8,12 +7,16 @@ export interface X25519KeyPair { export type KeyPair = X25519KeyPair; -export function generateX25519KeyPair(): X25519KeyPair { +export async function generateX25519KeyPair(): Promise { + const nacl = await import("tweetnacl"); + const kp = nacl.box.keyPair(); return { publicKey: kp.publicKey, privateKey: kp.secretKey }; } -export function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array { +export async function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Promise { + const nacl = await import("tweetnacl"); + // nacl.box.before returns shared key (Curve25519, XSalsa20-Poly1305 context). We use it as IKM into HKDF. return nacl.box.before(theirPublicKey, myPrivateKey); }