From 5032d92f8ef9df59aa6740da9cc0c76b710d66ac Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 21:24:36 +0200 Subject: [PATCH] =?UTF-8?q?WASM=20=D1=83=D1=81=D0=BA=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D0=BC=20=D1=88=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7=D0=B1=D0=B5?= =?UTF-8?q?=D0=B6=D0=B0=D0=BD=D0=B8=D1=8F=20backpressure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 117 ++++++++++++------------ package.json | 7 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 216d065..fb6ed29 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,54 +1,46 @@ -function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { - const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); - return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; -} +import { chacha20 } from "@noble/ciphers/chacha"; type KeyInput = Buffer | Uint8Array; +const senderAttached = new WeakSet(); +const receiverAttached = new WeakSet(); -async function importAesCtrKey(input: KeyInput): Promise { - const keyBytes = toArrayBuffer(input); - if (keyBytes.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); - } - - return crypto.subtle.importKey( - "raw", - keyBytes, - { name: "AES-CTR" }, - false, - ["encrypt", "decrypt"] - ); +function toUint8Array(input: KeyInput): Uint8Array { + const u8 = input instanceof Uint8Array ? input : new Uint8Array(input); + return new Uint8Array(u8.slice().buffer); } -function toBigIntTs(ts: unknown): bigint { - if (typeof ts === "bigint") return ts; - if (typeof ts === "number") return BigInt(ts); - return 0n; +function buildNonce(timestamp: unknown): Uint8Array { + const nonce = new Uint8Array(12); + const ts = typeof timestamp === "number" + ? timestamp + : typeof timestamp === "bigint" + ? Number(timestamp) + : 0; + new DataView(nonce.buffer).setUint32(8, ts >>> 0, false); + return nonce; } -/** - * 16-byte counter: - * [0..3] direction marker - * [4..11] frame timestamp - * [12..15] reserved - */ -function buildCounter(direction: number, timestamp: unknown): ArrayBuffer { - const iv = new Uint8Array(16); - const dv = new DataView(iv.buffer); - dv.setUint32(0, direction >>> 0, false); - dv.setBigUint64(4, toBigIntTs(timestamp), false); - dv.setUint32(12, 0, false); - return toArrayBuffer(iv); +function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer { + const nonce = buildNonce(timestamp); + const input = new Uint8Array(data); + // ChaCha20 симметричный: encrypt === decrypt, тот же размер + const output = chacha20(key, nonce, input); + return output.buffer as ArrayBuffer; } export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + if (senderAttached.has(sender)) { + return; + } + senderAttached.add(sender); - const anySender = sender as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const key = toUint8Array(keyInput); + if (key.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + } + const anySender = sender as any; if (!anySender.createEncodedStreams) { throw new Error("createEncodedStreams is not available on RTCRtpSender"); } @@ -56,15 +48,15 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) const { readable, writable } = anySender.createEncodedStreams(); const enc = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const encrypted = await crypto.subtle.encrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = encrypted; // same length - controller.enqueue(frame); + // Синхронный transform — нет async, нет накопления очереди + transform(frame, controller) { + try { + frame.data = processFrame(frame.data, key, frame.timestamp); + controller.enqueue(frame); + } catch (e) { + console.error("Sender E2EE frame failed:", e); + controller.enqueue(frame); + } } }); @@ -74,12 +66,17 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + if (receiverAttached.has(receiver)) { + return; + } + receiverAttached.add(receiver); - const anyReceiver = receiver as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const key = toUint8Array(keyInput); + if (key.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + } + const anyReceiver = receiver as any; if (!anyReceiver.createEncodedStreams) { throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); } @@ -87,15 +84,15 @@ export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: Key const { readable, writable } = anyReceiver.createEncodedStreams(); const dec = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const decrypted = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = decrypted; // same length - controller.enqueue(frame); + // Синхронный transform — нет async, нет накопления очереди + transform(frame, controller) { + try { + frame.data = processFrame(frame.data, key, frame.timestamp); + controller.enqueue(frame); + } catch (e) { + console.error("Receiver E2EE frame failed:", e); + controller.enqueue(frame); + } } }); diff --git a/package.json b/package.json index 9332cd4..06f0e09 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "build": { "electronUpdaterCompatibility": false, "extraResources": [ - { "from": "resources/", "to": "resources/" } + { + "from": "resources/", + "to": "resources/" + } ], "files": [ "node_modules/sqlite3/**/*", @@ -81,7 +84,7 @@ "@mantine/form": "^8.3.12", "@mantine/hooks": "^8.3.12", "@mantine/modals": "^8.3.12", - "@noble/ciphers": "^1.2.1", + "@noble/ciphers": "^1.3.0", "@noble/secp256k1": "^3.0.0", "@tabler/icons-react": "^3.31.0", "@types/crypto-js": "^4.2.2",