From d3cda685cd565652c00871252880c9ffd53c7eb5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 17:26:52 +0200 Subject: [PATCH] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 22 ++-- app/providers/CallProvider/audioE2EE.ts | 135 ++++++++++++-------- 2 files changed, 96 insertions(+), 61 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 1367d79..db027ec 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -12,7 +12,7 @@ import { modals } from "@mantine/modals"; import { Button, Flex, Text } from "@mantine/core"; import { useSound } from "@/app/hooks/useSound"; import useWindow from "@/app/hooks/useWindow"; -import { enableAudioE2EE } from "./audioE2EE"; +import { attachReceiverE2EE, attachSenderE2EE } from "./audioE2EE"; export interface CallContextValue { call: (callable: string) => void; @@ -298,7 +298,9 @@ export function CallProvider(props : CallProviderProps) { * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение */ peerConnectionRef.current = new RTCPeerConnection({ - iceServers: iceServersRef.current + iceServers: iceServersRef.current, + // @ts-ignore + encodedInsertableStreams: true }); /** * Подписываемся на ICE кандидат @@ -340,14 +342,18 @@ export function CallProvider(props : CallProviderProps) { * Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести, * когда мы установим WebRTC соединение */ - const localStream = await navigator.mediaDevices.getUserMedia({ - audio: true - }); - localStream.getTracks().forEach(track => { - peerConnectionRef.current?.addTrack(track, localStream); + const localStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const audioTrack = localStream.getAudioTracks()[0]; + + + const tx = peerConnectionRef.current.addTransceiver(audioTrack, { + direction: "sendrecv", + streams: [localStream] }); - await enableAudioE2EE(peerConnectionRef.current, Buffer.from(sharedSecret, 'hex')); + await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); + + await attachReceiverE2EE(tx.receiver, Buffer.from(sharedSecret, "hex")); /** * Отправляем свой оффер другой стороне */ diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index a35b94d..0d385ba 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -3,74 +3,103 @@ function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; } -export async function enableAudioE2EE(pc: RTCPeerConnection, keyBuffer: Buffer | Uint8Array) { - const raw = toArrayBuffer(keyBuffer); - if (raw.byteLength !== 32) throw new Error("E2EE key must be 32 bytes"); +type KeyInput = Buffer | Uint8Array; - const key = await crypto.subtle.importKey( + +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", - raw, + keyBytes, { name: "AES-CTR" }, false, ["encrypt", "decrypt"] ); +} - const makeCounter = (ts: number, dir: number) => { - const counter = new Uint8Array(16); - const dv = new DataView(counter.buffer); - dv.setUint32(0, dir, false); // разделяем направление - dv.setBigUint64(8, BigInt(ts), false); // nonce из timestamp кадра - return counter; +function toBigIntTs(ts: unknown): bigint { + if (typeof ts === "bigint") return ts; + if (typeof ts === "number") return BigInt(ts); + return 0n; +} + +/** + * 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); +} + +export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { + const key = await importAesCtrKey(keyInput); + + const anySender = sender as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; }; - const attachSender = (sender: RTCRtpSender) => { - // @ts-ignore Chromium/Electron API - if (!sender.createEncodedStreams) return; - // @ts-ignore - const { readable, writable } = sender.createEncodedStreams(); + if (!anySender.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpSender"); + } - const enc = new TransformStream({ - async transform(frame: any, controller) { - const counter = makeCounter(frame.timestamp ?? 0, 1); - const out = await crypto.subtle.encrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = new Uint8Array(out); // тот же размер - controller.enqueue(frame); - } - }); + const { readable, writable } = anySender.createEncodedStreams(); - readable.pipeThrough(enc).pipeTo(writable).catch(() => {}); + 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); + } + }); + + readable.pipeThrough(enc).pipeTo(writable).catch((e) => { + console.error("Sender E2EE pipeline failed:", e); + }); +} + +export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { + const key = await importAesCtrKey(keyInput); + + const anyReceiver = receiver as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; }; - const attachReceiver = (receiver: RTCRtpReceiver) => { - // @ts-ignore Chromium/Electron API - if (!receiver.createEncodedStreams) return; - // @ts-ignore - const { readable, writable } = receiver.createEncodedStreams(); + if (!anyReceiver.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); + } - const dec = new TransformStream({ - async transform(frame: any, controller) { - const counter = makeCounter(frame.timestamp ?? 0, 1); - const out = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = new Uint8Array(out); // тот же размер - controller.enqueue(frame); - } - }); + const { readable, writable } = anyReceiver.createEncodedStreams(); - readable.pipeThrough(dec).pipeTo(writable).catch(() => {}); - }; + 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); + } + }); - pc.getSenders().forEach((s) => s.track?.kind === "audio" && attachSender(s)); - pc.getReceivers().forEach((r) => r.track?.kind === "audio" && attachReceiver(r)); - - pc.addEventListener("track", (e) => { - if (e.track.kind === "audio") attachReceiver(e.receiver); + readable.pipeThrough(dec).pipeTo(writable).catch((e) => { + console.error("Receiver E2EE pipeline failed:", e); }); } \ No newline at end of file