mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Backend moved to a separate repository
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "./account";
|
||||
import { request } from "@/core/websocket";
|
||||
import type { DmEnvelope, User } from "@/core/types";
|
||||
import { fetchUserPublicKey } from "./crypto";
|
||||
import { fetchUsers, searchUsers } from "./users";
|
||||
|
||||
/**
|
||||
* Decrypt a DM envelope using client-side MEK unwrapping.
|
||||
* This delegates to the chats/dm module which has the updated implementation.
|
||||
*/
|
||||
export async function decryptDm(envelope: DmEnvelope): Promise<string> {
|
||||
// Import and use the updated implementation from chats/dm
|
||||
const { decrypt } = await import("./chats/dm");
|
||||
return decrypt(envelope);
|
||||
}
|
||||
|
||||
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 || [];
|
||||
}
|
||||
|
||||
// Re-export user functions for convenience
|
||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||
|
||||
/**
|
||||
* Send DM via WebSocket using transport encryption.
|
||||
* This delegates to the HTTP endpoint which handles envelope encryption on server.
|
||||
*/
|
||||
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||
// Import and use the updated implementation from chats/dm
|
||||
const { send } = await import("./chats/dm");
|
||||
return send(recipientId, recipientPublicKeyB64, plaintext, authToken, replyToId);
|
||||
}
|
||||
|
||||
|
||||
export async function deleteDmEnvelope(id: number, recipientId: number, authToken: string): Promise<void> {
|
||||
await request({
|
||||
type: "dmDelete",
|
||||
credentials: { scheme: "Bearer", credentials: authToken },
|
||||
data: { id, recipientId }
|
||||
});
|
||||
}
|
||||
|
||||
export interface DMConversationResponse {
|
||||
user: User;
|
||||
lastMessage: DmEnvelope;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export async function fetchDMConversations(token: string): Promise<DMConversationResponse[]> {
|
||||
const res = await fetch(`${API_BASE_URL}/dm/conversations`, {
|
||||
headers: getAuthHeaders(token, true)
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.conversations || [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Envelope Encryption (Private DMs with compliance support)
|
||||
// ============================================================================
|
||||
|
||||
interface TransportKey {
|
||||
key_id: string;
|
||||
public_key_b64: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
interface TransportEncryptedMessage {
|
||||
client_public_key_b64: string;
|
||||
nonce_b64: string;
|
||||
ciphertext_b64: string;
|
||||
}
|
||||
|
||||
let cachedTransportKey: TransportKey | null = null;
|
||||
|
||||
/**
|
||||
* Fetch current transport public key from messaging service.
|
||||
* Caches result with validation.
|
||||
*/
|
||||
export async function getTransportPublicKey(): Promise<TransportKey> {
|
||||
if (cachedTransportKey) {
|
||||
return cachedTransportKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/dm/key/transport/public`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const data: TransportKey = await response.json();
|
||||
cachedTransportKey = data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transport public key:", error);
|
||||
}
|
||||
|
||||
throw new Error("Failed to fetch transport public key");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a message using the transport public key (X25519 + ChaCha20).
|
||||
*/
|
||||
function encryptMessageWithTransportKey(
|
||||
plaintext: string | Uint8Array,
|
||||
transportPublicKeyB64: string
|
||||
): { nonce_b64: string; ciphertext_b64: string; client_public_key_b64: string } {
|
||||
const tweetnacl = require("tweetnacl");
|
||||
|
||||
// Convert plaintext to bytes if string
|
||||
const plaintextBytes = typeof plaintext === "string" ? new TextEncoder().encode(plaintext) : plaintext;
|
||||
|
||||
// Generate ephemeral keypair for this message
|
||||
const ephemeralKeypair = tweetnacl.box.keyPair();
|
||||
|
||||
// Decode transport public key
|
||||
const transportPublicKeyBytes = new Uint8Array(
|
||||
atob(transportPublicKeyB64)
|
||||
.split("")
|
||||
.map((c: string) => c.charCodeAt(0))
|
||||
);
|
||||
|
||||
// Perform ECDH (shared secret via tweetnacl's box)
|
||||
const nonce = tweetnacl.randomBytes(24);
|
||||
const ciphertext = tweetnacl.box(plaintextBytes, nonce, transportPublicKeyBytes, ephemeralKeypair.secretKey);
|
||||
|
||||
// Encode to base64
|
||||
const nonce_b64 = btoa(String.fromCharCode.apply(null, Array.from(nonce) as number[]));
|
||||
const ciphertext_b64 = btoa(String.fromCharCode.apply(null, Array.from(ciphertext) as number[]));
|
||||
const client_public_key_b64 = btoa(
|
||||
String.fromCharCode.apply(null, Array.from(ephemeralKeypair.publicKey) as number[])
|
||||
);
|
||||
|
||||
return { nonce_b64, ciphertext_b64, client_public_key_b64 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt plaintext with transport public key for sending to server.
|
||||
* Server will handle envelope encryption (MEK generation and wrapping).
|
||||
*/
|
||||
export async function encryptMessageForTransport(plaintext: string): Promise<TransportEncryptedMessage> {
|
||||
const transportKey = await getTransportPublicKey();
|
||||
return encryptMessageWithTransportKey(plaintext, transportKey.public_key_b64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an encrypted DM message using envelope encryption.
|
||||
* Client encrypts with transport key, server handles envelope encryption.
|
||||
*/
|
||||
export async function sendEncryptedDM(
|
||||
recipientId: number,
|
||||
plaintext: string,
|
||||
token: string,
|
||||
replyToId?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Client-side transport encryption
|
||||
const { client_public_key_b64, nonce_b64, ciphertext_b64 } =
|
||||
await encryptMessageForTransport(plaintext);
|
||||
|
||||
// Send to server
|
||||
const response = await fetch(`${API_BASE_URL}/dm/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getAuthHeaders(token, true)
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recipient_id: recipientId,
|
||||
client_public_key_b64,
|
||||
transport_nonce_b64: nonce_b64,
|
||||
transport_ciphertext_b64: ciphertext_b64,
|
||||
reply_to_id: replyToId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to send encrypted DM:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encrypted conversation history with another user.
|
||||
*/
|
||||
export async function getEncryptedConversation(
|
||||
otherUserId: number,
|
||||
token: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
const url = new URL(`${API_BASE_URL}/dm/conversation/${otherUserId}`);
|
||||
url.searchParams.append("limit", String(limit));
|
||||
url.searchParams.append("offset", String(offset));
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: getAuthHeaders(token, true)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch encrypted conversation with user ${otherUserId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an encrypted message.
|
||||
*/
|
||||
export async function deleteEncryptedDM(messageId: number, token: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/dm/${messageId}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(token, true)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete encrypted DM ${messageId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached keys (useful on logout).
|
||||
*/
|
||||
export function clearCachedKeys(): void {
|
||||
cachedTransportKey = null;
|
||||
}
|
||||
Reference in New Issue
Block a user