Шифрование звонков E2EE

This commit is contained in:
RoyceDa
2026-03-20 17:18:46 +02:00
parent 9f8840e077
commit 427f2e9e33
3 changed files with 80 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import { modals } from "@mantine/modals";
import { Button, Flex, Text } from "@mantine/core"; import { Button, Flex, Text } from "@mantine/core";
import { useSound } from "@/app/hooks/useSound"; import { useSound } from "@/app/hooks/useSound";
import useWindow from "@/app/hooks/useWindow"; import useWindow from "@/app/hooks/useWindow";
import { enableAudioE2EE } from "./audioE2EE";
export interface CallContextValue { export interface CallContextValue {
call: (callable: string) => void; call: (callable: string) => void;
@@ -345,6 +346,8 @@ export function CallProvider(props : CallProviderProps) {
localStream.getTracks().forEach(track => { localStream.getTracks().forEach(track => {
peerConnectionRef.current?.addTrack(track, localStream); peerConnectionRef.current?.addTrack(track, localStream);
}); });
await enableAudioE2EE(peerConnectionRef.current, Buffer.from(sharedSecret, 'hex'));
/** /**
* Отправляем свой оффер другой стороне * Отправляем свой оффер другой стороне
*/ */

View File

@@ -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);
});
}

View File

@@ -5,6 +5,7 @@
"composite": true, "composite": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".", "baseUrl": ".",
"lib": ["DOM"],
"esModuleInterop": true, "esModuleInterop": true,
"types": ["electron-vite/node"], "types": ["electron-vite/node"],
"paths": { "paths": {