Restructure code

This commit is contained in:
2025-10-02 18:18:08 +03:00
Unverified
parent 0d33c344b8
commit a24270b37a
85 changed files with 17 additions and 15 deletions
-171
View File
@@ -1,171 +0,0 @@
import { API_BASE_URL } from "../core/config";
import { getAuthHeaders } from "../auth/api";
import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric";
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric";
import { randomBytes } from "../utils/crypto/kdf";
import { getCurrentKeys } from "../auth/crypto";
import { request } from "../core/websocket";
import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../core/types";
import { b64, ub64 } from "../utils/utils";
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
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);
}
export async function fetchUsers(token: string): Promise<User[]> {
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(token, true) });
if (!res.ok) return [];
const data = await res.json();
return data.users || [];
}
export async function fetchUserPublicKey(userId: number, token: string): Promise<string | null> {
const res = await fetch(`${API_BASE_URL}/crypto/public-key/of/${userId}`, { headers: getAuthHeaders(token, true) });
if (!res.ok) return null;
const data = await res.json();
return data.publicKey;
}
export async function fetchDMHistory(userId: number, token: string, limit: number = 50): Promise<DmEnvelope[]> {
const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=${limit}`, {
headers: getAuthHeaders(token, true)
});
if (!response.ok) return [];
const data = await response.json();
return data.messages || [];
}
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
const keys = getCurrentKeys();
if (!keys) throw new Error("Keys not initialized");
// Encryption key
const mk = randomBytes(32);
const wkSalt = randomBytes(16);
const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
const wk = await importAesGcmKey(wkRaw);
// Encrypt the message
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext));
const wrap = await aesGcmEncrypt(wk, mk);
const payload: SendDMRequest = {
recipientId: recipientId,
iv: b64(encMsg.iv),
ciphertext: b64(encMsg.ciphertext),
salt: b64(wkSalt),
iv2: b64(wrap.iv),
wrappedMk: b64(wrap.ciphertext)
};
if (replyToId) payload.replyToId = replyToId;
await request({
type: "dmSend",
credentials: {
scheme: "Bearer",
credentials: authToken
},
data: payload
});
}
export async function sendDmWithFiles(recipientId: number, recipientPublicKeyB64: string, plaintextJson: string, files: File[], token: string): Promise<void> {
const keys = getCurrentKeys();
if (!keys) throw new Error("Keys not initialized");
const mk = randomBytes(32);
const wkSalt = randomBytes(16);
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
const wk = await importAesGcmKey(wkRaw);
const wrap = await aesGcmEncrypt(wk, mk);
const form = new FormData();
const names: string[] = [];
function sliceBuffer(u8: Uint8Array): ArrayBuffer {
return (u8.buffer as ArrayBuffer).slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
}
for (const f of files) {
// Encrypt file with same mk
const data = new Uint8Array(await f.arrayBuffer());
const enc = await aesGcmEncrypt(await importAesGcmKey(mk), data);
const blob = new Blob([sliceBuffer(enc.iv), sliceBuffer(enc.ciphertext)], { type: "application/octet-stream" });
const serverName = f.name; // server uses provided name
names.push(serverName);
form.append("files", new File([blob], serverName));
}
form.append("fileNames", JSON.stringify(names));
// Merge files metadata into plaintext JSON and encrypt
let obj: DmEncryptedJSON;
try {
obj = JSON.parse(plaintextJson);
} catch {
obj = { type: "text", data: { content: String(plaintextJson) } };
}
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(JSON.stringify(obj)));
form.append("dm_payload", JSON.stringify({
recipientId: recipientId,
iv: b64(encMsg.iv),
ciphertext: b64(encMsg.ciphertext),
salt: b64(wkSalt),
iv2: b64(wrap.iv),
wrappedMk: b64(wrap.ciphertext)
} satisfies BaseDmEnvelope));
await fetch(`${API_BASE_URL}/dm/send`, {
method: "POST",
headers: getAuthHeaders(token, false),
body: form
});
}
export async function editDmEnvelope(id: number, recipientPublicKeyB64: string, newPlaintextJson: string, authToken: string): Promise<void> {
const keys = getCurrentKeys();
if (!keys) throw new Error("Keys not initialized");
// We cannot reuse the old mk safely without knowing it; generate a fresh mk and wrap
const mk = randomBytes(32);
const wkSalt = randomBytes(16);
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
const wk = await importAesGcmKey(wkRaw);
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(newPlaintextJson));
const wrap = await aesGcmEncrypt(wk, mk);
await request({
type: "dmEdit",
credentials: { scheme: "Bearer", credentials: authToken },
data: {
id,
iv: b64(encMsg.iv),
ciphertext: b64(encMsg.ciphertext),
iv2: b64(wrap.iv),
wrappedMk: b64(wrap.ciphertext),
salt: b64(wkSalt)
}
} as DMEditRequest);
}
export async function deleteDmEnvelope(id: number, recipientId: number, authToken: string): Promise<void> {
await request({
type: "dmDelete",
credentials: { scheme: "Bearer", credentials: authToken },
data: { id, recipientId }
});
}
-128
View File
@@ -1,128 +0,0 @@
import { getAuthHeaders } from "../auth/api";
import { API_BASE_URL } from "../core/config";
import type { UserProfile } from "../core/types";
export interface ProfileData {
profile_picture?: string;
nickname?: string;
description?: string;
}
export interface UploadResponse {
profile_picture_url: string;
}
/**
* Loads user profile data from the server
*/
export async function loadProfile(token: string): Promise<ProfileData | null> {
try {
const response = await fetch(`${API_BASE_URL}/user/profile`, {
headers: getAuthHeaders(token)
});
if (response.ok) {
const data = await response.json();
// Map backend fields to frontend fields
return {
profile_picture: data.profile_picture,
nickname: data.username,
description: data.bio
};
}
return null;
} catch (error) {
console.error('Error loading profile:', error);
return null;
}
}
/**
* Uploads a profile picture to the server
*/
export async function uploadProfilePicture(token: string, file: Blob): Promise<UploadResponse | null> {
try {
const formData = new FormData();
formData.append('profile_picture', file, 'profile_picture.jpg');
const response = await fetch(`${API_BASE_URL}/upload-profile-picture`, {
method: 'POST',
body: formData,
headers: getAuthHeaders(token, false)
});
if (response.ok) {
return await response.json();
}
return null;
} catch (error) {
console.error('Upload error:', error);
return null;
}
}
/**
* Updates user profile information
*/
export async function updateProfile(token: string, data: Partial<ProfileData>): Promise<boolean> {
try {
// Map frontend fields to backend fields
const backendData = {
nickname: data.nickname,
description: data.description
};
const response = await fetch(`${API_BASE_URL}/user/profile`, {
method: 'PUT',
headers: {
...getAuthHeaders(token),
'Content-Type': 'application/json'
},
body: JSON.stringify(backendData)
});
return response.ok;
} catch (error) {
console.error('Error updating profile:', error);
return false;
}
}
/**
* Updates user bio
*/
export async function updateBio(token: string, bio: string): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/user/bio`, {
method: 'PUT',
headers: getAuthHeaders(token),
body: JSON.stringify({ bio })
});
return response.ok;
} catch (error) {
console.error('Error updating bio:', error);
return false;
}
}
/**
* Fetches user profile data by username
*/
export async function fetchUserProfile(token: string, username: string): Promise<UserProfile | null> {
try {
const response = await fetch(`${API_BASE_URL}/user/${username}`, {
headers: getAuthHeaders(token)
});
if (response.ok) {
return await response.json();
}
return null;
} catch (error) {
console.error('Error fetching user profile:', error);
return null;
}
}