mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Refactor the code
This commit is contained in:
@@ -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
@@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Vendored
+23
@@ -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
|
||||||
// ---------------
|
// ---------------
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"]);
|
||||||
}
|
}
|
||||||
Vendored
-2
@@ -2,5 +2,3 @@ declare module "tweetnacl" {
|
|||||||
const nacl: any;
|
const nacl: any;
|
||||||
export default nacl;
|
export default nacl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
|
|||||||
Reference in New Issue
Block a user