From 2151a0b4a1cc389903c23345374115a99c17ec20 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 25 Aug 2025 18:16:47 +0300 Subject: [PATCH] Refactor the code --- frontend/src/auth/api.ts | 18 ++++- frontend/src/auth/crypto.ts | 34 ++++---- frontend/src/chat/chat.ts | 25 ++---- frontend/src/chat/contextMenu.ts | 84 ++++++++------------ frontend/src/chat/dm.ts | 124 +++++++++++++++++------------- frontend/src/chat/panel.ts | 12 +-- frontend/src/core/types.d.ts | 23 ++++++ frontend/src/crypto/asymmetric.ts | 6 +- frontend/src/crypto/kdf.ts | 19 +++-- frontend/src/crypto/symmetric.ts | 16 ++-- frontend/src/crypto/types.d.ts | 4 +- frontend/src/utils/utils.ts | 9 +++ frontend/src/websocket.ts | 15 ++++ frontend/tsconfig.json | 1 + 14 files changed, 220 insertions(+), 170 deletions(-) diff --git a/frontend/src/auth/api.ts b/frontend/src/auth/api.ts index 692c3b5..2855fb4 100644 --- a/frontend/src/auth/api.ts +++ b/frontend/src/auth/api.ts @@ -1,7 +1,8 @@ import { API_BASE_URL } from "../core/config"; import { showLogin } from "../navigation"; -import type { Headers, User } from "../core/types"; +import type { Headers, User, WebSocketMessage } from "../core/types"; import { clearAlerts } from "./auth"; +import { request } from "../websocket"; /** * Current authenticated user information @@ -23,6 +24,21 @@ export let authToken: string | null = null; export function setUser(token: string, user: User) { authToken = token currentUser = user + + try { + const payload: WebSocketMessage = { + type: "ping", + credentials: { + scheme: "Bearer", + credentials: authToken + }, + data: {} + } + + request(payload).then(() => { + console.log("Ping succeeded") + }) + } catch {} } /** diff --git a/frontend/src/auth/crypto.ts b/frontend/src/auth/crypto.ts index 65687e1..902cf7b 100644 --- a/frontend/src/auth/crypto.ts +++ b/frontend/src/auth/crypto.ts @@ -2,18 +2,12 @@ import { API_BASE_URL } from "../core/config"; import { getAuthHeaders } from "./api"; import { generateX25519KeyPair } from "../crypto/asymmetric"; import { encryptBackupWithPassword, decryptBackupWithPassword, encodeBlob, decodeBlob } from "../crypto/backup"; +import { b64, ub64 } from "../utils/utils"; +import type { BackupBlob, UploadPublicKeyRequest } from "../core/types"; let currentPublicKey: Uint8Array | null = null; let currentPrivateKey: Uint8Array | null = null; -function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); } -function ub64(s: string): Uint8Array { - const bin = atob(s); - const arr = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); - return arr; -} - async function fetchPublicKey(): Promise { const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers: getAuthHeaders(true) }); if (!res.ok) return null; @@ -23,25 +17,37 @@ async function fetchPublicKey(): Promise { } async function uploadPublicKey(publicKey: Uint8Array): Promise { + const payload: UploadPublicKeyRequest = { + publicKey: b64(publicKey) + } + await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "POST", headers: getAuthHeaders(true), - body: JSON.stringify({ publicKey: b64(publicKey) }) + body: JSON.stringify(payload) }); } async function fetchBackupBlob(): Promise { - const res = await fetch(`${API_BASE_URL}/crypto/backup`, { method: "GET", headers: getAuthHeaders(true) }); - if (!res.ok) return null; - const data = await res.json(); - return data?.blob ?? null; + const res = await fetch(`${API_BASE_URL}/crypto/backup`, { + method: "GET", + headers: getAuthHeaders(true) + }); + if (res.ok) { + const response: BackupBlob = await res.json(); + return response.blob; + } else { + return null; + } } async function uploadBackupBlob(blobJson: string): Promise { + const payload: BackupBlob = { blob: blobJson } + await fetch(`${API_BASE_URL}/crypto/backup`, { method: "POST", headers: getAuthHeaders(true), - body: JSON.stringify({ blob: blobJson }) + body: JSON.stringify(payload) }); } diff --git a/frontend/src/chat/chat.ts b/frontend/src/chat/chat.ts index a7c92c9..914362b 100644 --- a/frontend/src/chat/chat.ts +++ b/frontend/src/chat/chat.ts @@ -6,15 +6,14 @@ */ import { API_BASE_URL } from "../core/config"; -import { websocket } from "../websocket"; +import { request } from "../websocket"; import type { Message, Messages, WebSocketMessage } from "../core/types"; import { formatTime } from "../utils/utils"; import { show as showContextMenu } from "./contextMenu"; import { show as showUserProfileDialog } from "./profileDialog"; import defaultAvatar from "../resources/images/default-avatar.png"; import { authToken, currentUser, getAuthHeaders } from "../auth/api"; -import { ChatPanelController, PublicChatPanel } from "./panel"; -import type { Tabs } from "mdui/components/tabs"; +import { PublicChatPanel } from "./panel"; /** * Adds a new message to the chat interface @@ -156,12 +155,12 @@ export function loadMessages(): void { /** * Sends a message via WebSocket */ -export function sendMessage(): void { +export async function sendMessage(): Promise { const input = document.querySelector('.message-input') as HTMLInputElement; const message = input.value.trim(); if (message) { - const payload: WebSocketMessage = { + const response = await request({ data: { content: message }, @@ -170,20 +169,12 @@ export function sendMessage(): void { credentials: authToken! }, type: "sendMessage" - } + }) - let callback: ((e: MessageEvent) => void) | null = null - callback = (e) => { - websocket.removeEventListener("message", callback!); - const response: WebSocketMessage = JSON.parse(e.data) - console.log(response) - if (!response.error) { - input.value = ""; - } + console.log(response) + if (!response.error) { + input.value = ""; } - websocket.addEventListener("message", callback); - - websocket.send(JSON.stringify(payload)); } } diff --git a/frontend/src/chat/contextMenu.ts b/frontend/src/chat/contextMenu.ts index 92f2056..579cbba 100644 --- a/frontend/src/chat/contextMenu.ts +++ b/frontend/src/chat/contextMenu.ts @@ -5,8 +5,8 @@ * @version 1.0.0 */ -import { websocket } from "../websocket"; -import type { Message, WebSocketMessage } from "../core/types"; +import { request } from "../websocket"; +import type { Message } from "../core/types"; import { showSuccess, showError } from "../utils/notification"; import { delay, id } from "../utils/utils"; import type { Dialog } from "mdui/components/dialog"; @@ -184,7 +184,7 @@ function hideEditDialog(): void { * Saves the edited message * @private */ -function saveEdit(): void { +async function saveEdit(): Promise { if (!currentMessage) return; const textField = editDialog.querySelector('#edit-message-input') as TextField; @@ -195,7 +195,7 @@ function saveEdit(): void { return; } - const payload: WebSocketMessage = { + const response = await request({ type: "editMessage", data: { message_id: currentMessage.id, @@ -205,22 +205,14 @@ function saveEdit(): void { scheme: "Bearer", credentials: authToken! } - }; + }); - let callback: ((e: MessageEvent) => void) | null = null; - callback = (e) => { - websocket.removeEventListener("message", callback!); - const response: WebSocketMessage = JSON.parse(e.data); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Message edited successfully'); - hideEditDialog(); - } - }; - websocket.addEventListener("message", callback); - websocket.send(JSON.stringify(payload)); + if (response.error) { + showError(response.error.detail); + } else { + showSuccess('Message edited successfully'); + hideEditDialog(); + } } /** @@ -257,7 +249,7 @@ function hideReplyDialog(): void { * Sends the reply message * @private */ -function sendReply(): void { +async function sendReply(): Promise { if (!currentMessage) return; const textField = replyDialog.querySelector('#reply-message-input') as TextField; @@ -268,7 +260,7 @@ function sendReply(): void { return; } - const payload: WebSocketMessage = { + const response = await request({ type: "replyMessage", data: { content: content, @@ -278,25 +270,17 @@ function sendReply(): void { scheme: "Bearer", credentials: authToken! } - }; + }); - let callback: ((e: MessageEvent) => void) | null = null; - callback = (e) => { - websocket.removeEventListener("message", callback!); - const response: WebSocketMessage = JSON.parse(e.data); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Reply sent successfully'); - hideReplyDialog(); - if (textField) { - textField.value = ''; - } + if (response.error) { + showError(response.error.detail); + } else { + showSuccess('Reply sent successfully'); + hideReplyDialog(); + if (textField) { + textField.value = ''; } - }; - websocket.addEventListener("message", callback); - websocket.send(JSON.stringify(payload)); + } } /** @@ -304,12 +288,12 @@ function sendReply(): void { * @param {Message} message - The message to delete * @private */ -function deleteMessage(message: Message): void { +async function deleteMessage(message: Message): Promise { if (!confirm('Are you sure you want to delete this message?')) { return; } - const payload: WebSocketMessage = { + const response = await request({ type: "deleteMessage", data: { message_id: message.id @@ -318,21 +302,13 @@ function deleteMessage(message: Message): void { scheme: "Bearer", credentials: authToken! } - }; + }); - let callback: ((e: MessageEvent) => void) | null = null; - callback = (e) => { - websocket.removeEventListener("message", callback!); - const response: WebSocketMessage = JSON.parse(e.data); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Message deleted successfully'); - } - }; - websocket.addEventListener("message", callback); - websocket.send(JSON.stringify(payload)); + if (response.error) { + showError(response.error.detail); + } else { + showSuccess('Message deleted successfully'); + } } init(); \ No newline at end of file diff --git a/frontend/src/chat/dm.ts b/frontend/src/chat/dm.ts index 9e67c46..ebf3f86 100644 --- a/frontend/src/chat/dm.ts +++ b/frontend/src/chat/dm.ts @@ -1,21 +1,14 @@ import { API_BASE_URL } from "../core/config"; -import { getAuthHeaders } from "../auth/api"; -import { DmPanel, ChatPanelController } from "./panel"; +import { authToken, getAuthHeaders } from "../auth/api"; +import { DmPanel } from "./panel"; import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric"; import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric"; import { randomBytes } from "../crypto/kdf"; import { getCurrentKeys } from "../auth/crypto"; -import { websocket } from "../websocket"; -import type { WebSocketMessage } from "../core/types"; +import { request, websocket } from "../websocket"; +import type { FetchDMResponse, SendDMRequest, WebSocketMessage } from "../core/types"; import type { Tabs } from "mdui/components/tabs"; - -function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); } -function ub64(s: string): Uint8Array { - const bin = atob(s); - const arr = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); - return arr; -} +import { b64, ub64 } from "../utils/utils"; export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string): Promise { const keys = getCurrentKeys(); @@ -27,11 +20,12 @@ export async function sendDm(recipientId: number, recipientPublicKeyB64: string, 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(true), body: JSON.stringify({ - recipientId, + recipientId: recipientId, iv: b64(encMsg.iv), ciphertext: b64(encMsg.ciphertext), salt: b64(wkSalt), @@ -56,19 +50,30 @@ export interface DmEnvelope { export async function fetchDm(since?: number): Promise { const url = new URL(`${API_BASE_URL}/dm/fetch`); if (since) url.searchParams.set("since", String(since)); - const res = await fetch(url, { headers: getAuthHeaders(true) }); - if (!res.ok) return []; - const data = await res.json(); - return data.messages ?? []; + + const response = await fetch(url, { + headers: getAuthHeaders(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 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)); + + // Decrypt const msg = await aesGcmDecrypt(await importAesGcmKey(mk), ub64(envelope.iv), ub64(envelope.ciphertext)); return new TextDecoder().decode(msg); } @@ -95,54 +100,66 @@ async function loadUsers() { // WebSocket realtime send const keys = getCurrentKeys(); if (!keys) return; + + // Encryption key const mk = randomBytes(32); const wkSalt = randomBytes(16); const shared = ecdhSharedSecret(keys.privateKey, ub64(activeDm.publicKey)); const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); const wk = await importAesGcmKey(wkRaw); + + // Encrypt the message const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(text)); const wrap = await aesGcmEncrypt(wk, mk); - const payload: WebSocketMessage = { + + const payload: SendDMRequest = { + recipientId: activeDm.userId, + iv: b64(encMsg.iv), + ciphertext: b64(encMsg.ciphertext), + salt: b64(wkSalt), + iv2: b64(wrap.iv), + wrappedMk: b64(wrap.ciphertext) + } + + request({ type: "dmSend", - credentials: { scheme: "Bearer", credentials: (await import("../auth/api")).authToken! }, - data: { - recipientId: activeDm.userId, - iv: b64(encMsg.iv), - ciphertext: b64(encMsg.ciphertext), - salt: b64(wkSalt), - iv2: b64(wrap.iv), - wrappedMk: b64(wrap.ciphertext) - } - }; - websocket.send(JSON.stringify(payload)); + credentials: { + scheme: "Bearer", + credentials: authToken! + }, + data: payload + }); } }, async () => { // Load DM history for the active conversation if (!activeDm?.publicKey) return; - const res = await fetch(`${API_BASE_URL}/dm/history/${activeDm.userId}`, { headers: getAuthHeaders(true) }); - if (!res.ok) return; - const data = await res.json(); - const messages: DmEnvelope[] = data.messages || []; - const container = document.getElementById("chat-messages")!; - container.innerHTML = ""; - for (const env of messages) { - try { - const text = await decryptDm(env, activeDm.publicKey); - const div = document.createElement("div"); - const isAuthor = env.senderId !== activeDm.userId; - div.className = `message ${isAuthor ? "sent" : "received"}`; - const inner = document.createElement("div"); - inner.className = "message-inner"; - const content = document.createElement("div"); - content.className = "message-content"; - content.textContent = text; - inner.appendChild(content); - div.appendChild(inner); - container.appendChild(div); - } catch {} + const response = await fetch(`${API_BASE_URL}/dm/history/${activeDm.userId}`, { + headers: getAuthHeaders(true) + }); + if (response.ok) { + const data = await response.json(); + const messages: DmEnvelope[] = data.messages || []; + const container = document.getElementById("chat-messages")!; + container.innerHTML = ""; + for (const env of messages) { + try { + const text = await decryptDm(env, activeDm.publicKey); + const div = document.createElement("div"); + const isAuthor = env.senderId !== activeDm.userId; + div.className = `message ${isAuthor ? "sent" : "received"}`; + const inner = document.createElement("div"); + inner.className = "message-inner"; + const content = document.createElement("div"); + content.className = "message-content"; + content.textContent = text; + inner.appendChild(content); + div.appendChild(inner); + container.appendChild(div); + } catch {} + } + container.scrollTop = container.scrollHeight; } - container.scrollTop = container.scrollHeight; } ); } @@ -182,9 +199,10 @@ init(); // realtime incoming DMs websocket.addEventListener("message", async (e) => { try { - const msg = JSON.parse((e as MessageEvent).data); - if (msg?.type === "dmNew" && activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) { + const msg: WebSocketMessage = JSON.parse((e as MessageEvent).data); + if (msg.type === "dmNew" && activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) { const plaintext = await decryptDm(msg.data, activeDm.publicKey!); + const container = document.getElementById("chat-messages")!; const div = document.createElement("div"); const isAuthor = msg.data.senderId !== activeDm.userId; diff --git a/frontend/src/chat/panel.ts b/frontend/src/chat/panel.ts index 6abd786..9a99522 100644 --- a/frontend/src/chat/panel.ts +++ b/frontend/src/chat/panel.ts @@ -1,6 +1,6 @@ import { authToken, currentUser } from "../auth/api"; import type { WebSocketMessage } from "../core/types"; -import { websocket } from "../websocket"; +import { request } from "../websocket"; import { loadMessages } from "./chat"; const titleEl = document.getElementById("chat-name")!; @@ -73,15 +73,7 @@ export class PublicChatPanel extends ChatPanelController { credentials: { scheme: "Bearer", credentials: authToken! }, type: "sendMessage" }; - await new Promise((resolve) => { - let callback: ((e: MessageEvent) => void) | null = null; - callback = (e) => { - websocket.removeEventListener("message", callback!); - resolve(); - }; - websocket.addEventListener("message", callback); - websocket.send(JSON.stringify(payload)); - }); + await request(payload); } protected loadMessages(): void { diff --git a/frontend/src/core/types.d.ts b/frontend/src/core/types.d.ts index 4447a1d..c76c51b 100644 --- a/frontend/src/core/types.d.ts +++ b/frontend/src/core/types.d.ts @@ -5,6 +5,8 @@ * @version 1.0.0 */ +import type { DmEnvelope } from "../chat/dm"; + /** * HTTP headers object type * @typedef {Object.} Headers @@ -136,6 +138,19 @@ export interface RegisterRequest { confirm_password: string; } +export interface UploadPublicKeyRequest { + publicKey: string; +} + +export interface SendDMRequest { + recipientId: number; + iv: string; + ciphertext: string; + salt: string; + iv2: string; + wrappedMk: string; +} + // Responses /** @@ -149,6 +164,14 @@ export interface LoginResponse { token: string; } +export interface BackupBlob { + blob: string; +} + +export interface FetchDMResponse { + messages: DmEnvelope[] +} + // --------------- // WebSocket types // --------------- diff --git a/frontend/src/crypto/asymmetric.ts b/frontend/src/crypto/asymmetric.ts index 2aca8f5..a086efa 100644 --- a/frontend/src/crypto/asymmetric.ts +++ b/frontend/src/crypto/asymmetric.ts @@ -17,7 +17,5 @@ export function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8 } export async function deriveWrappingKey(sharedSecret: Uint8Array, salt: Uint8Array, info: Uint8Array): Promise { - return hkdfExtractAndExpand(sharedSecret.buffer, salt, info, 32); -} - - + return hkdfExtractAndExpand(sharedSecret.buffer as ArrayBuffer, salt, info, 32); +} \ No newline at end of file diff --git a/frontend/src/crypto/kdf.ts b/frontend/src/crypto/kdf.ts index c3c6912..133d90c 100644 --- a/frontend/src/crypto/kdf.ts +++ b/frontend/src/crypto/kdf.ts @@ -3,9 +3,10 @@ export async function importPassword(password: string): Promise { return crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey", "deriveBits"]); } -export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array, iterations = 210_000): Promise { +export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array | ArrayBuffer, iterations = 210_000): Promise { + const saltBuffer = salt instanceof Uint8Array ? salt.buffer as ArrayBuffer : salt; return crypto.subtle.deriveKey( - { name: "PBKDF2", salt, iterations, hash: "SHA-256" }, + { name: "PBKDF2", salt: saltBuffer, iterations, hash: "SHA-256" }, passwordKey, { name: "AES-GCM", length: 256 }, false, @@ -13,9 +14,13 @@ export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array, iterat ); } -export async function hkdfExtractAndExpand(inputKeyMaterial: ArrayBuffer, salt: Uint8Array, info: Uint8Array, length = 32): Promise { - const ikmKey = await crypto.subtle.importKey("raw", inputKeyMaterial, { name: "HKDF" }, false, ["deriveBits"]); - const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikmKey, length * 8); +export async function hkdfExtractAndExpand(inputKeyMaterial: Uint8Array | ArrayBuffer, salt: Uint8Array | ArrayBuffer, info: Uint8Array | ArrayBuffer, length = 32): Promise { + const inputBuffer = inputKeyMaterial instanceof Uint8Array ? inputKeyMaterial.buffer as ArrayBuffer : inputKeyMaterial; + const saltBuffer = salt instanceof Uint8Array ? salt.buffer as ArrayBuffer : salt; + const infoBuffer = info instanceof Uint8Array ? info.buffer as ArrayBuffer : info; + + const ikmKey = await crypto.subtle.importKey("raw", inputBuffer, { name: "HKDF" }, false, ["deriveBits"]); + const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: saltBuffer, info: infoBuffer }, ikmKey, length * 8); return new Uint8Array(bits); } @@ -23,6 +28,4 @@ export function randomBytes(length: number): Uint8Array { const out = new Uint8Array(length); crypto.getRandomValues(out); return out; -} - - +} \ No newline at end of file diff --git a/frontend/src/crypto/symmetric.ts b/frontend/src/crypto/symmetric.ts index fc75380..f0b1780 100644 --- a/frontend/src/crypto/symmetric.ts +++ b/frontend/src/crypto/symmetric.ts @@ -3,17 +3,21 @@ export interface AesGcmCiphertext { ciphertext: Uint8Array; } -export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array): Promise { +export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array | ArrayBuffer): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); - const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext); + const plaintextBuffer = plaintext instanceof Uint8Array ? plaintext.buffer as ArrayBuffer : plaintext; + const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintextBuffer); return { iv, ciphertext: new Uint8Array(ct) }; } -export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array, ciphertext: Uint8Array): Promise { - const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext); +export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array | ArrayBuffer, ciphertext: Uint8Array | ArrayBuffer): Promise { + const ivBuffer = iv instanceof Uint8Array ? iv.buffer as ArrayBuffer : iv; + const ciphertextBuffer = ciphertext instanceof Uint8Array ? ciphertext.buffer as ArrayBuffer : ciphertext; + const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: ivBuffer }, key, ciphertextBuffer); return new Uint8Array(pt); } -export async function importAesGcmKey(rawKey: Uint8Array): Promise { - return crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); +export async function importAesGcmKey(rawKey: Uint8Array | ArrayBuffer): Promise { + const keyBuffer = rawKey instanceof Uint8Array ? rawKey.buffer as ArrayBuffer : rawKey; + return crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); } \ No newline at end of file diff --git a/frontend/src/crypto/types.d.ts b/frontend/src/crypto/types.d.ts index 5a531a0..13bbec3 100644 --- a/frontend/src/crypto/types.d.ts +++ b/frontend/src/crypto/types.d.ts @@ -1,6 +1,4 @@ declare module "tweetnacl" { const nacl: any; export default nacl; -} - - +} \ No newline at end of file diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 184c0ef..b3b10de 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -32,6 +32,15 @@ export function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } + +export function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); } +export function ub64(s: string): Uint8Array { + const bin = atob(s); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return arr; +} + export function id(id: string): T { return document.getElementById(id) as unknown as T } \ No newline at end of file diff --git a/frontend/src/websocket.ts b/frontend/src/websocket.ts index 28b0199..afd28a2 100644 --- a/frontend/src/websocket.ts +++ b/frontend/src/websocket.ts @@ -7,6 +7,7 @@ import { handleWebSocketMessage } from "./chat/chat"; import { API_WS_BASE_URL } from "./core/config"; +import type { WebSocketMessage } from "./core/types"; import { delay } from "./utils/utils"; /** @@ -29,6 +30,20 @@ function create(): WebSocket { */ export let websocket: WebSocket = create(); +export function request(payload: WebSocketMessage): Promise { + return new Promise((resolve, reject) => { + let listener: ((e: MessageEvent) => void) | null = null; + listener = (e) => { + resolve(JSON.parse(e.data)); + websocket.removeEventListener("message", listener!); + } + websocket.addEventListener("message", listener); + websocket.send(JSON.stringify(payload)) + + setTimeout(() => reject("Request timed out"), 10000); + }) +} + /** * This function will wait 3 seconds and them attempts to reconnect the WebSocket. * If it fails, tries again in an endless loop until the connection is established diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 28db92d..cd5be70 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -14,6 +14,7 @@ "noEmit": true, /* Linting */ + "strict": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true