mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Improve code splitting
This commit is contained in:
@@ -5,56 +5,15 @@ import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/s
|
|||||||
import { randomBytes } from "../utils/crypto/kdf";
|
import { randomBytes } from "../utils/crypto/kdf";
|
||||||
import { getCurrentKeys } from "../auth/crypto";
|
import { getCurrentKeys } from "../auth/crypto";
|
||||||
import { request } from "../core/websocket";
|
import { request } from "../core/websocket";
|
||||||
import type { FetchDMResponse, SendDMRequest, DmEnvelope, User } from "../core/types";
|
import type { SendDMRequest, DmEnvelope, User } from "../core/types";
|
||||||
import { b64, ub64 } from "../utils/utils";
|
import { b64, ub64 } from "../utils/utils";
|
||||||
|
|
||||||
export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string, 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 = 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(plaintext));
|
|
||||||
const wrap = await aesGcmEncrypt(wk, mk);
|
|
||||||
|
|
||||||
await fetch(`${API_BASE_URL}/dm/send`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: getAuthHeaders(token, true),
|
|
||||||
body: JSON.stringify({
|
|
||||||
recipientId: recipientId,
|
|
||||||
iv: b64(encMsg.iv),
|
|
||||||
ciphertext: b64(encMsg.ciphertext),
|
|
||||||
salt: b64(wkSalt),
|
|
||||||
iv2: b64(wrap.iv),
|
|
||||||
wrappedMk: b64(wrap.ciphertext)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchDm(since: number | undefined, token: string): Promise<DmEnvelope[]> {
|
|
||||||
const url = new URL(`${API_BASE_URL}/dm/fetch`);
|
|
||||||
if (since) url.searchParams.set("since", String(since));
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: getAuthHeaders(token, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
// Obtain the key
|
||||||
const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64));
|
const shared = await 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));
|
||||||
@@ -94,7 +53,7 @@ export async function sendDMViaWebSocket(recipientId: number, recipientPublicKey
|
|||||||
// Encryption key
|
// Encryption key
|
||||||
const mk = randomBytes(32);
|
const mk = randomBytes(32);
|
||||||
const wkSalt = randomBytes(16);
|
const wkSalt = randomBytes(16);
|
||||||
const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||||
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);
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export async function ensureKeysOnLogin(password: string, token: string): Promis
|
|||||||
currentPublicKey = serverPub;
|
currentPublicKey = serverPub;
|
||||||
} else {
|
} else {
|
||||||
// We don't have the corresponding public key from server; regenerate pair to resync
|
// We don't have the corresponding public key from server; regenerate pair to resync
|
||||||
const pair = generateX25519KeyPair();
|
const pair = await generateX25519KeyPair();
|
||||||
currentPublicKey = pair.publicKey;
|
currentPublicKey = pair.publicKey;
|
||||||
currentPrivateKey = pair.privateKey;
|
currentPrivateKey = pair.privateKey;
|
||||||
await uploadPublicKey(currentPublicKey, token);
|
await uploadPublicKey(currentPublicKey, token);
|
||||||
@@ -107,7 +107,7 @@ export async function ensureKeysOnLogin(password: string, token: string): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First-time setup: generate keys and upload
|
// First-time setup: generate keys and upload
|
||||||
const pair = generateX25519KeyPair();
|
const pair = await generateX25519KeyPair();
|
||||||
currentPublicKey = pair.publicKey;
|
currentPublicKey = pair.publicKey;
|
||||||
currentPrivateKey = pair.privateKey;
|
currentPrivateKey = pair.privateKey;
|
||||||
await uploadPublicKey(currentPublicKey, token);
|
await uploadPublicKey(currentPublicKey, token);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Message as MessageType } from "../../../core/types";
|
|||||||
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
||||||
import Quote from "../core/Quote";
|
import Quote from "../core/Quote";
|
||||||
import { parse } from "marked";
|
import { parse } from "marked";
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
@@ -16,13 +15,15 @@ interface MessageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false, isDm = false }: MessageProps) {
|
export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false, isDm = false }: MessageProps) {
|
||||||
const [formattedMessage, setFormattedMessage] = useState({ __html: DOMPurify.sanitize(message.content).trim() });
|
const [formattedMessage, setFormattedMessage] = useState({ __html: "" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const DOMPurify = await import("dompurify");
|
||||||
|
|
||||||
setFormattedMessage({
|
setFormattedMessage({
|
||||||
__html: DOMPurify.sanitize(
|
__html: DOMPurify.default.sanitize(
|
||||||
await parse(message.content)
|
await parse(message.content.trim())
|
||||||
).trim()
|
).trim()
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import nacl from "tweetnacl";
|
|
||||||
import { hkdfExtractAndExpand } from "../crypto/kdf";
|
import { hkdfExtractAndExpand } from "../crypto/kdf";
|
||||||
|
|
||||||
export interface X25519KeyPair {
|
export interface X25519KeyPair {
|
||||||
@@ -8,12 +7,16 @@ export interface X25519KeyPair {
|
|||||||
|
|
||||||
export type KeyPair = X25519KeyPair;
|
export type KeyPair = X25519KeyPair;
|
||||||
|
|
||||||
export function generateX25519KeyPair(): X25519KeyPair {
|
export async function generateX25519KeyPair(): Promise<X25519KeyPair> {
|
||||||
|
const nacl = await import("tweetnacl");
|
||||||
|
|
||||||
const kp = nacl.box.keyPair();
|
const kp = nacl.box.keyPair();
|
||||||
return { publicKey: kp.publicKey, privateKey: kp.secretKey };
|
return { publicKey: kp.publicKey, privateKey: kp.secretKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array {
|
export async function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Promise<Uint8Array> {
|
||||||
|
const nacl = await import("tweetnacl");
|
||||||
|
|
||||||
// nacl.box.before returns shared key (Curve25519, XSalsa20-Poly1305 context). We use it as IKM into HKDF.
|
// nacl.box.before returns shared key (Curve25519, XSalsa20-Poly1305 context). We use it as IKM into HKDF.
|
||||||
return nacl.box.before(theirPublicKey, myPrivateKey);
|
return nacl.box.before(theirPublicKey, myPrivateKey);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user