Refactor the code

This commit is contained in:
2025-08-25 18:16:47 +03:00
Unverified
parent bddf34de2d
commit 2151a0b4a1
14 changed files with 220 additions and 170 deletions
+17 -1
View File
@@ -1,7 +1,8 @@
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../core/config";
import { showLogin } from "../navigation"; import { showLogin } from "../navigation";
import type { Headers, User } from "../core/types"; import type { Headers, User, WebSocketMessage } from "../core/types";
import { clearAlerts } from "./auth"; import { clearAlerts } from "./auth";
import { request } from "../websocket";
/** /**
* Current authenticated user information * Current authenticated user information
@@ -23,6 +24,21 @@ export let authToken: string | null = null;
export function setUser(token: string, user: User) { export function setUser(token: string, user: User) {
authToken = token authToken = token
currentUser = user currentUser = user
try {
const payload: WebSocketMessage = {
type: "ping",
credentials: {
scheme: "Bearer",
credentials: authToken
},
data: {}
}
request(payload).then(() => {
console.log("Ping succeeded")
})
} catch {}
} }
/** /**
+20 -14
View File
@@ -2,18 +2,12 @@ import { API_BASE_URL } from "../core/config";
import { getAuthHeaders } from "./api"; import { getAuthHeaders } from "./api";
import { generateX25519KeyPair } from "../crypto/asymmetric"; import { generateX25519KeyPair } from "../crypto/asymmetric";
import { encryptBackupWithPassword, decryptBackupWithPassword, encodeBlob, decodeBlob } from "../crypto/backup"; 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 currentPublicKey: Uint8Array | null = null;
let currentPrivateKey: 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<Uint8Array | null> { async function fetchPublicKey(): Promise<Uint8Array | null> {
const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers: getAuthHeaders(true) }); const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers: getAuthHeaders(true) });
if (!res.ok) return null; if (!res.ok) return null;
@@ -23,25 +17,37 @@ async function fetchPublicKey(): Promise<Uint8Array | null> {
} }
async function uploadPublicKey(publicKey: Uint8Array): Promise<void> { async function uploadPublicKey(publicKey: Uint8Array): Promise<void> {
const payload: UploadPublicKeyRequest = {
publicKey: b64(publicKey)
}
await fetch(`${API_BASE_URL}/crypto/public-key`, { await fetch(`${API_BASE_URL}/crypto/public-key`, {
method: "POST", method: "POST",
headers: getAuthHeaders(true), headers: getAuthHeaders(true),
body: JSON.stringify({ publicKey: b64(publicKey) }) body: JSON.stringify(payload)
}); });
} }
async function fetchBackupBlob(): Promise<string | null> { async function fetchBackupBlob(): Promise<string | null> {
const res = await fetch(`${API_BASE_URL}/crypto/backup`, { method: "GET", headers: getAuthHeaders(true) }); const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
if (!res.ok) return null; method: "GET",
const data = await res.json(); headers: getAuthHeaders(true)
return data?.blob ?? null; });
if (res.ok) {
const response: BackupBlob = await res.json();
return response.blob;
} else {
return null;
}
} }
async function uploadBackupBlob(blobJson: string): Promise<void> { async function uploadBackupBlob(blobJson: string): Promise<void> {
const payload: BackupBlob = { blob: blobJson }
await fetch(`${API_BASE_URL}/crypto/backup`, { await fetch(`${API_BASE_URL}/crypto/backup`, {
method: "POST", method: "POST",
headers: getAuthHeaders(true), headers: getAuthHeaders(true),
body: JSON.stringify({ blob: blobJson }) body: JSON.stringify(payload)
}); });
} }
+8 -17
View File
@@ -6,15 +6,14 @@
*/ */
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../core/config";
import { websocket } from "../websocket"; import { request } from "../websocket";
import type { Message, Messages, WebSocketMessage } from "../core/types"; import type { Message, Messages, WebSocketMessage } from "../core/types";
import { formatTime } from "../utils/utils"; import { formatTime } from "../utils/utils";
import { show as showContextMenu } from "./contextMenu"; import { show as showContextMenu } from "./contextMenu";
import { show as showUserProfileDialog } from "./profileDialog"; import { show as showUserProfileDialog } from "./profileDialog";
import defaultAvatar from "../resources/images/default-avatar.png"; import defaultAvatar from "../resources/images/default-avatar.png";
import { authToken, currentUser, getAuthHeaders } from "../auth/api"; import { authToken, currentUser, getAuthHeaders } from "../auth/api";
import { ChatPanelController, PublicChatPanel } from "./panel"; import { PublicChatPanel } from "./panel";
import type { Tabs } from "mdui/components/tabs";
/** /**
* Adds a new message to the chat interface * Adds a new message to the chat interface
@@ -156,12 +155,12 @@ export function loadMessages(): void {
/** /**
* Sends a message via WebSocket * Sends a message via WebSocket
*/ */
export function sendMessage(): void { export async function sendMessage(): Promise<void> {
const input = document.querySelector('.message-input') as HTMLInputElement; const input = document.querySelector('.message-input') as HTMLInputElement;
const message = input.value.trim(); const message = input.value.trim();
if (message) { if (message) {
const payload: WebSocketMessage = { const response = await request({
data: { data: {
content: message content: message
}, },
@@ -170,20 +169,12 @@ export function sendMessage(): void {
credentials: authToken! credentials: authToken!
}, },
type: "sendMessage" type: "sendMessage"
} })
let callback: ((e: MessageEvent) => void) | null = null console.log(response)
callback = (e) => { if (!response.error) {
websocket.removeEventListener("message", callback!); input.value = "";
const response: WebSocketMessage = JSON.parse(e.data)
console.log(response)
if (!response.error) {
input.value = "";
}
} }
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
} }
} }
+30 -54
View File
@@ -5,8 +5,8 @@
* @version 1.0.0 * @version 1.0.0
*/ */
import { websocket } from "../websocket"; import { request } from "../websocket";
import type { Message, WebSocketMessage } from "../core/types"; import type { Message } from "../core/types";
import { showSuccess, showError } from "../utils/notification"; import { showSuccess, showError } from "../utils/notification";
import { delay, id } from "../utils/utils"; import { delay, id } from "../utils/utils";
import type { Dialog } from "mdui/components/dialog"; import type { Dialog } from "mdui/components/dialog";
@@ -184,7 +184,7 @@ function hideEditDialog(): void {
* Saves the edited message * Saves the edited message
* @private * @private
*/ */
function saveEdit(): void { async function saveEdit(): Promise<void> {
if (!currentMessage) return; if (!currentMessage) return;
const textField = editDialog.querySelector('#edit-message-input') as TextField; const textField = editDialog.querySelector('#edit-message-input') as TextField;
@@ -195,7 +195,7 @@ function saveEdit(): void {
return; return;
} }
const payload: WebSocketMessage = { const response = await request({
type: "editMessage", type: "editMessage",
data: { data: {
message_id: currentMessage.id, message_id: currentMessage.id,
@@ -205,22 +205,14 @@ function saveEdit(): void {
scheme: "Bearer", scheme: "Bearer",
credentials: authToken! credentials: authToken!
} }
}; });
let callback: ((e: MessageEvent) => void) | null = null; if (response.error) {
callback = (e) => { showError(response.error.detail);
websocket.removeEventListener("message", callback!); } else {
const response: WebSocketMessage = JSON.parse(e.data); showSuccess('Message edited successfully');
hideEditDialog();
if (response.error) { }
showError(response.error.detail);
} else {
showSuccess('Message edited successfully');
hideEditDialog();
}
};
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
} }
/** /**
@@ -257,7 +249,7 @@ function hideReplyDialog(): void {
* Sends the reply message * Sends the reply message
* @private * @private
*/ */
function sendReply(): void { async function sendReply(): Promise<void> {
if (!currentMessage) return; if (!currentMessage) return;
const textField = replyDialog.querySelector('#reply-message-input') as TextField; const textField = replyDialog.querySelector('#reply-message-input') as TextField;
@@ -268,7 +260,7 @@ function sendReply(): void {
return; return;
} }
const payload: WebSocketMessage = { const response = await request({
type: "replyMessage", type: "replyMessage",
data: { data: {
content: content, content: content,
@@ -278,25 +270,17 @@ function sendReply(): void {
scheme: "Bearer", scheme: "Bearer",
credentials: authToken! credentials: authToken!
} }
}; });
let callback: ((e: MessageEvent) => void) | null = null; if (response.error) {
callback = (e) => { showError(response.error.detail);
websocket.removeEventListener("message", callback!); } else {
const response: WebSocketMessage = JSON.parse(e.data); showSuccess('Reply sent successfully');
hideReplyDialog();
if (response.error) { if (textField) {
showError(response.error.detail); textField.value = '';
} 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 * @param {Message} message - The message to delete
* @private * @private
*/ */
function deleteMessage(message: Message): void { async function deleteMessage(message: Message): Promise<void> {
if (!confirm('Are you sure you want to delete this message?')) { if (!confirm('Are you sure you want to delete this message?')) {
return; return;
} }
const payload: WebSocketMessage = { const response = await request({
type: "deleteMessage", type: "deleteMessage",
data: { data: {
message_id: message.id message_id: message.id
@@ -318,21 +302,13 @@ function deleteMessage(message: Message): void {
scheme: "Bearer", scheme: "Bearer",
credentials: authToken! credentials: authToken!
} }
}; });
let callback: ((e: MessageEvent) => void) | null = null; if (response.error) {
callback = (e) => { showError(response.error.detail);
websocket.removeEventListener("message", callback!); } else {
const response: WebSocketMessage = JSON.parse(e.data); showSuccess('Message deleted successfully');
}
if (response.error) {
showError(response.error.detail);
} else {
showSuccess('Message deleted successfully');
}
};
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
} }
init(); init();
+71 -53
View File
@@ -1,21 +1,14 @@
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../core/config";
import { getAuthHeaders } from "../auth/api"; import { authToken, getAuthHeaders } from "../auth/api";
import { DmPanel, ChatPanelController } from "./panel"; import { DmPanel } from "./panel";
import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric"; import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric";
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric"; import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric";
import { randomBytes } from "../crypto/kdf"; import { randomBytes } from "../crypto/kdf";
import { getCurrentKeys } from "../auth/crypto"; import { getCurrentKeys } from "../auth/crypto";
import { websocket } from "../websocket"; import { request, websocket } from "../websocket";
import type { WebSocketMessage } from "../core/types"; import type { FetchDMResponse, SendDMRequest, WebSocketMessage } from "../core/types";
import type { Tabs } from "mdui/components/tabs"; import type { Tabs } from "mdui/components/tabs";
import { b64, ub64 } from "../utils/utils";
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;
}
export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string): Promise<void> { export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string): Promise<void> {
const keys = getCurrentKeys(); const keys = getCurrentKeys();
@@ -27,11 +20,12 @@ export async function sendDm(recipientId: number, recipientPublicKeyB64: string,
const wk = await importAesGcmKey(wkRaw); const wk = await importAesGcmKey(wkRaw);
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext)); const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext));
const wrap = await aesGcmEncrypt(wk, mk); const wrap = await aesGcmEncrypt(wk, mk);
await fetch(`${API_BASE_URL}/dm/send`, { await fetch(`${API_BASE_URL}/dm/send`, {
method: "POST", method: "POST",
headers: getAuthHeaders(true), headers: getAuthHeaders(true),
body: JSON.stringify({ body: JSON.stringify({
recipientId, recipientId: recipientId,
iv: b64(encMsg.iv), iv: b64(encMsg.iv),
ciphertext: b64(encMsg.ciphertext), ciphertext: b64(encMsg.ciphertext),
salt: b64(wkSalt), salt: b64(wkSalt),
@@ -56,19 +50,30 @@ export interface DmEnvelope {
export async function fetchDm(since?: number): Promise<DmEnvelope[]> { export async function fetchDm(since?: number): Promise<DmEnvelope[]> {
const url = new URL(`${API_BASE_URL}/dm/fetch`); const url = new URL(`${API_BASE_URL}/dm/fetch`);
if (since) url.searchParams.set("since", String(since)); if (since) url.searchParams.set("since", String(since));
const res = await fetch(url, { headers: getAuthHeaders(true) });
if (!res.ok) return []; const response = await fetch(url, {
const data = await res.json(); headers: getAuthHeaders(true)
return data.messages ?? []; });
if (response.ok) {
const data: FetchDMResponse = await response.json();
return data.messages ?? [];
} else {
return [];
}
} }
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> { export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
const keys = getCurrentKeys(); const keys = getCurrentKeys();
if (!keys) throw new Error("Keys not initialized"); if (!keys) throw new Error("Keys not initialized");
// Obtain the key
const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64)); const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64));
const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1])); const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1]));
const wk = await importAesGcmKey(wkRaw); const wk = await importAesGcmKey(wkRaw);
const mk = await aesGcmDecrypt(wk, ub64(envelope.iv2), ub64(envelope.wrappedMk)); 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)); const msg = await aesGcmDecrypt(await importAesGcmKey(mk), ub64(envelope.iv), ub64(envelope.ciphertext));
return new TextDecoder().decode(msg); return new TextDecoder().decode(msg);
} }
@@ -95,54 +100,66 @@ async function loadUsers() {
// WebSocket realtime send // WebSocket realtime send
const keys = getCurrentKeys(); const keys = getCurrentKeys();
if (!keys) return; if (!keys) return;
// Encryption key
const mk = randomBytes(32); const mk = randomBytes(32);
const wkSalt = randomBytes(16); const wkSalt = randomBytes(16);
const shared = ecdhSharedSecret(keys.privateKey, ub64(activeDm.publicKey)); const shared = ecdhSharedSecret(keys.privateKey, ub64(activeDm.publicKey));
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
const wk = await importAesGcmKey(wkRaw); const wk = await importAesGcmKey(wkRaw);
// Encrypt the message
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(text)); const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(text));
const wrap = await aesGcmEncrypt(wk, mk); 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", type: "dmSend",
credentials: { scheme: "Bearer", credentials: (await import("../auth/api")).authToken! }, credentials: {
data: { scheme: "Bearer",
recipientId: activeDm.userId, credentials: authToken!
iv: b64(encMsg.iv), },
ciphertext: b64(encMsg.ciphertext), data: payload
salt: b64(wkSalt), });
iv2: b64(wrap.iv),
wrappedMk: b64(wrap.ciphertext)
}
};
websocket.send(JSON.stringify(payload));
} }
}, },
async () => { async () => {
// Load DM history for the active conversation // Load DM history for the active conversation
if (!activeDm?.publicKey) return; if (!activeDm?.publicKey) return;
const res = await fetch(`${API_BASE_URL}/dm/history/${activeDm.userId}`, { headers: getAuthHeaders(true) }); const response = await fetch(`${API_BASE_URL}/dm/history/${activeDm.userId}`, {
if (!res.ok) return; headers: getAuthHeaders(true)
const data = await res.json(); });
const messages: DmEnvelope[] = data.messages || []; if (response.ok) {
const container = document.getElementById("chat-messages")!; const data = await response.json();
container.innerHTML = ""; const messages: DmEnvelope[] = data.messages || [];
for (const env of messages) { const container = document.getElementById("chat-messages")!;
try { container.innerHTML = "";
const text = await decryptDm(env, activeDm.publicKey); for (const env of messages) {
const div = document.createElement("div"); try {
const isAuthor = env.senderId !== activeDm.userId; const text = await decryptDm(env, activeDm.publicKey);
div.className = `message ${isAuthor ? "sent" : "received"}`; const div = document.createElement("div");
const inner = document.createElement("div"); const isAuthor = env.senderId !== activeDm.userId;
inner.className = "message-inner"; div.className = `message ${isAuthor ? "sent" : "received"}`;
const content = document.createElement("div"); const inner = document.createElement("div");
content.className = "message-content"; inner.className = "message-inner";
content.textContent = text; const content = document.createElement("div");
inner.appendChild(content); content.className = "message-content";
div.appendChild(inner); content.textContent = text;
container.appendChild(div); inner.appendChild(content);
} catch {} div.appendChild(inner);
container.appendChild(div);
} catch {}
}
container.scrollTop = container.scrollHeight;
} }
container.scrollTop = container.scrollHeight;
} }
); );
} }
@@ -182,9 +199,10 @@ init();
// realtime incoming DMs // realtime incoming DMs
websocket.addEventListener("message", async (e) => { websocket.addEventListener("message", async (e) => {
try { try {
const msg = JSON.parse((e as MessageEvent).data); const msg: WebSocketMessage = JSON.parse((e as MessageEvent).data);
if (msg?.type === "dmNew" && activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) { if (msg.type === "dmNew" && activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) {
const plaintext = await decryptDm(msg.data, activeDm.publicKey!); const plaintext = await decryptDm(msg.data, activeDm.publicKey!);
const container = document.getElementById("chat-messages")!; const container = document.getElementById("chat-messages")!;
const div = document.createElement("div"); const div = document.createElement("div");
const isAuthor = msg.data.senderId !== activeDm.userId; const isAuthor = msg.data.senderId !== activeDm.userId;
+2 -10
View File
@@ -1,6 +1,6 @@
import { authToken, currentUser } from "../auth/api"; import { authToken, currentUser } from "../auth/api";
import type { WebSocketMessage } from "../core/types"; import type { WebSocketMessage } from "../core/types";
import { websocket } from "../websocket"; import { request } from "../websocket";
import { loadMessages } from "./chat"; import { loadMessages } from "./chat";
const titleEl = document.getElementById("chat-name")!; const titleEl = document.getElementById("chat-name")!;
@@ -73,15 +73,7 @@ export class PublicChatPanel extends ChatPanelController {
credentials: { scheme: "Bearer", credentials: authToken! }, credentials: { scheme: "Bearer", credentials: authToken! },
type: "sendMessage" type: "sendMessage"
}; };
await new Promise<void>((resolve) => { await request(payload);
let callback: ((e: MessageEvent) => void) | null = null;
callback = (e) => {
websocket.removeEventListener("message", callback!);
resolve();
};
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
});
} }
protected loadMessages(): void { protected loadMessages(): void {
+23
View File
@@ -5,6 +5,8 @@
* @version 1.0.0 * @version 1.0.0
*/ */
import type { DmEnvelope } from "../chat/dm";
/** /**
* HTTP headers object type * HTTP headers object type
* @typedef {Object.<string, string>} Headers * @typedef {Object.<string, string>} Headers
@@ -136,6 +138,19 @@ export interface RegisterRequest {
confirm_password: string; confirm_password: string;
} }
export interface UploadPublicKeyRequest {
publicKey: string;
}
export interface SendDMRequest {
recipientId: number;
iv: string;
ciphertext: string;
salt: string;
iv2: string;
wrappedMk: string;
}
// Responses // Responses
/** /**
@@ -149,6 +164,14 @@ export interface LoginResponse {
token: string; token: string;
} }
export interface BackupBlob {
blob: string;
}
export interface FetchDMResponse {
messages: DmEnvelope[]
}
// --------------- // ---------------
// WebSocket types // WebSocket types
// --------------- // ---------------
+1 -3
View File
@@ -17,7 +17,5 @@ export function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8
} }
export async function deriveWrappingKey(sharedSecret: Uint8Array, salt: Uint8Array, info: Uint8Array): Promise<Uint8Array> { export async function deriveWrappingKey(sharedSecret: Uint8Array, salt: Uint8Array, info: Uint8Array): Promise<Uint8Array> {
return hkdfExtractAndExpand(sharedSecret.buffer, salt, info, 32); return hkdfExtractAndExpand(sharedSecret.buffer as ArrayBuffer, salt, info, 32);
} }
+10 -7
View File
@@ -3,9 +3,10 @@ export async function importPassword(password: string): Promise<CryptoKey> {
return crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey", "deriveBits"]); return crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey", "deriveBits"]);
} }
export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array, iterations = 210_000): Promise<CryptoKey> { export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array | ArrayBuffer, iterations = 210_000): Promise<CryptoKey> {
const saltBuffer = salt instanceof Uint8Array ? salt.buffer as ArrayBuffer : salt;
return crypto.subtle.deriveKey( return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, { name: "PBKDF2", salt: saltBuffer, iterations, hash: "SHA-256" },
passwordKey, passwordKey,
{ name: "AES-GCM", length: 256 }, { name: "AES-GCM", length: 256 },
false, 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<Uint8Array> { export async function hkdfExtractAndExpand(inputKeyMaterial: Uint8Array | ArrayBuffer, salt: Uint8Array | ArrayBuffer, info: Uint8Array | ArrayBuffer, length = 32): Promise<Uint8Array> {
const ikmKey = await crypto.subtle.importKey("raw", inputKeyMaterial, { name: "HKDF" }, false, ["deriveBits"]); const inputBuffer = inputKeyMaterial instanceof Uint8Array ? inputKeyMaterial.buffer as ArrayBuffer : inputKeyMaterial;
const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikmKey, length * 8); 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); return new Uint8Array(bits);
} }
@@ -24,5 +29,3 @@ export function randomBytes(length: number): Uint8Array {
crypto.getRandomValues(out); crypto.getRandomValues(out);
return out; return out;
} }
+10 -6
View File
@@ -3,17 +3,21 @@ export interface AesGcmCiphertext {
ciphertext: Uint8Array; ciphertext: Uint8Array;
} }
export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array): Promise<AesGcmCiphertext> { export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array | ArrayBuffer): Promise<AesGcmCiphertext> {
const iv = crypto.getRandomValues(new Uint8Array(12)); 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) }; return { iv, ciphertext: new Uint8Array(ct) };
} }
export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> { export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array | ArrayBuffer, ciphertext: Uint8Array | ArrayBuffer): Promise<Uint8Array> {
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext); 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); return new Uint8Array(pt);
} }
export async function importAesGcmKey(rawKey: Uint8Array): Promise<CryptoKey> { export async function importAesGcmKey(rawKey: Uint8Array | ArrayBuffer): Promise<CryptoKey> {
return crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); const keyBuffer = rawKey instanceof Uint8Array ? rawKey.buffer as ArrayBuffer : rawKey;
return crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
} }
-2
View File
@@ -2,5 +2,3 @@ declare module "tweetnacl" {
const nacl: any; const nacl: any;
export default nacl; export default nacl;
} }
+9
View File
@@ -32,6 +32,15 @@ export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); 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<T extends Element = HTMLElement>(id: string): T { export function id<T extends Element = HTMLElement>(id: string): T {
return document.getElementById(id) as unknown as T return document.getElementById(id) as unknown as T
} }
+15
View File
@@ -7,6 +7,7 @@
import { handleWebSocketMessage } from "./chat/chat"; import { handleWebSocketMessage } from "./chat/chat";
import { API_WS_BASE_URL } from "./core/config"; import { API_WS_BASE_URL } from "./core/config";
import type { WebSocketMessage } from "./core/types";
import { delay } from "./utils/utils"; import { delay } from "./utils/utils";
/** /**
@@ -29,6 +30,20 @@ function create(): WebSocket {
*/ */
export let websocket: WebSocket = create(); export let websocket: WebSocket = create();
export function request(payload: WebSocketMessage): Promise<WebSocketMessage> {
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. * 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 * If it fails, tries again in an endless loop until the connection is established
+1
View File
@@ -14,6 +14,7 @@
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true