diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 36202f1..1367d79 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -12,6 +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"; export interface CallContextValue { call: (callable: string) => void; @@ -345,6 +346,8 @@ export function CallProvider(props : CallProviderProps) { localStream.getTracks().forEach(track => { peerConnectionRef.current?.addTrack(track, localStream); }); + + await enableAudioE2EE(peerConnectionRef.current, Buffer.from(sharedSecret, 'hex')); /** * Отправляем свой оффер другой стороне */ diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts new file mode 100644 index 0000000..a35b94d --- /dev/null +++ b/app/providers/CallProvider/audioE2EE.ts @@ -0,0 +1,76 @@ +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; +} + +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"); + + const key = await crypto.subtle.importKey( + "raw", + raw, + { 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; + }; + + const attachSender = (sender: RTCRtpSender) => { + // @ts-ignore Chromium/Electron API + if (!sender.createEncodedStreams) return; + // @ts-ignore + const { readable, writable } = sender.createEncodedStreams(); + + 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); + } + }); + + readable.pipeThrough(enc).pipeTo(writable).catch(() => {}); + }; + + const attachReceiver = (receiver: RTCRtpReceiver) => { + // @ts-ignore Chromium/Electron API + if (!receiver.createEncodedStreams) return; + // @ts-ignore + const { readable, writable } = receiver.createEncodedStreams(); + + 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); + } + }); + + readable.pipeThrough(dec).pipeTo(writable).catch(() => {}); + }; + + 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); + }); +} \ No newline at end of file diff --git a/tsconfig.web.json b/tsconfig.web.json index eb4c3b3..9b4503a 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,6 +5,7 @@ "composite": true, "jsx": "react-jsx", "baseUrl": ".", + "lib": ["DOM"], "esModuleInterop": true, "types": ["electron-vite/node"], "paths": {