From 9f8840e077bd6092b457fee5d02a8e363892f15a Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 16:46:23 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B7=D0=B2?= =?UTF-8?q?=D1=83=D0=BA=D0=BE=D0=B2=20=D0=B8=D0=B7=20=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=83=D1=80=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/useSound.ts | 6 +++--- lib/main/ipcs/ipcRuntime.ts | 13 +++++++++++++ lib/main/main.ts | 1 + lib/preload/index.d.ts | 2 +- lib/preload/preload.ts | 13 +++++-------- 5 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 lib/main/ipcs/ipcRuntime.ts diff --git a/app/hooks/useSound.ts b/app/hooks/useSound.ts index 44ffac8..f4473e2 100644 --- a/app/hooks/useSound.ts +++ b/app/hooks/useSound.ts @@ -14,7 +14,7 @@ export function useSound() { audioRef.current.load(); }; - const playSound = (sound : string, loop: boolean = false) => { + const playSound = async (sound : string, loop: boolean = false) => { try { if(loop){ if (!loopingAudioRef.current) { @@ -24,7 +24,7 @@ export function useSound() { loopingAudioRef.current.loop = true; } - const url = window.mediaApi.getSoundUrl(sound); + const url = await window.mediaApi.getSoundUrl(sound); const player = loopingAudioRef.current; player.src = url; @@ -43,7 +43,7 @@ export function useSound() { audioRef.current.loop = loop; } - const url = window.mediaApi.getSoundUrl(sound); + const url = await window.mediaApi.getSoundUrl(sound); const player = audioRef.current; stopSound(); diff --git a/lib/main/ipcs/ipcRuntime.ts b/lib/main/ipcs/ipcRuntime.ts new file mode 100644 index 0000000..5c13ceb --- /dev/null +++ b/lib/main/ipcs/ipcRuntime.ts @@ -0,0 +1,13 @@ +import { app, ipcMain } from "electron"; +import path from "path"; + +/** + * Получить директорию с ресурсами приложения + */ +ipcMain.handle('runtime:get-resources', () => { + const isDev = !app.isPackaged && process.env['ELECTRON_RENDERER_URL']; + if(isDev){ + return path.join(process.cwd(), "resources") + } + return path.join(process.resourcesPath, "resources"); +}); \ No newline at end of file diff --git a/lib/main/main.ts b/lib/main/main.ts index 8a550ef..90c71db 100644 --- a/lib/main/main.ts +++ b/lib/main/main.ts @@ -8,6 +8,7 @@ import './ipcs/ipcUpdate' import './ipcs/ipcNotification' import './ipcs/ipcDevice' import './ipcs/ipcCore' +import './ipcs/ipcRuntime' import { Tray } from 'electron/main' import { join } from 'path' import { Logger } from './logger' diff --git a/lib/preload/index.d.ts b/lib/preload/index.d.ts index 284361b..ed3d8e1 100644 --- a/lib/preload/index.d.ts +++ b/lib/preload/index.d.ts @@ -14,7 +14,7 @@ declare global { deviceName: string; deviceId: string; mediaApi: { - getSoundUrl: (fileName: string) => string; + getSoundUrl: (fileName: string) => Promise; }; } } diff --git a/lib/preload/preload.ts b/lib/preload/preload.ts index dbe848a..288699a 100644 --- a/lib/preload/preload.ts +++ b/lib/preload/preload.ts @@ -6,12 +6,9 @@ import path from 'node:path' import fs from "node:fs"; -function resolveSound(fileName: string) { - const isDev = !process.env.APP_PACKAGED; // или свой флаг dev - const fullPath = isDev - ? path.join(process.cwd(), "resources", "sounds", fileName) - : path.join(process.resourcesPath, "resources", "sounds", fileName); - +async function resolveSound(fileName: string) { + const resourcesPath = await ipcRenderer.invoke('runtime:get-resources'); + const fullPath = path.join(resourcesPath, "sounds", fileName); if (!fs.existsSync(fullPath)) { throw new Error(`Sound not found: ${fullPath}`); } @@ -32,7 +29,7 @@ const exposeContext = async () => { } }); contextBridge.exposeInMainWorld("mediaApi", { - getSoundUrl: (fileName: string) => { + getSoundUrl: async (fileName: string) => { return resolveSound(fileName); } }); @@ -44,7 +41,7 @@ const exposeContext = async () => { window.api = api; window.shell = shell; window.mediaApi = { - getSoundUrl: (fileName: string) => { + getSoundUrl: async (fileName: string) => { return resolveSound(fileName); } } From 427f2e9e33566799bd3f3c1ca3928d9eb8035cfd Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 17:18:46 +0200 Subject: [PATCH 2/7] =?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=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=20E2EE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 3 + app/providers/CallProvider/audioE2EE.ts | 76 +++++++++++++++++++++ tsconfig.web.json | 1 + 3 files changed, 80 insertions(+) create mode 100644 app/providers/CallProvider/audioE2EE.ts 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": { From d3cda685cd565652c00871252880c9ffd53c7eb5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 17:26:52 +0200 Subject: [PATCH 3/7] =?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 From 59d40e3005b0e4e73186c1720b24ee838ed496dc Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:18:04 +0200 Subject: [PATCH 4/7] =?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 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index db027ec..62996bc 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -324,7 +324,12 @@ export function CallProvider(props : CallProviderProps) { } } - peerConnectionRef.current.ontrack = (event) => { + peerConnectionRef.current.ontrack = async (event) => { + try { + await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecret, "hex")); + } catch (e) { + console.error("attachReceiverE2EE failed:", e); + } /** * При получении медиа-трека с другой стороны */ @@ -347,13 +352,12 @@ export function CallProvider(props : CallProviderProps) { const tx = peerConnectionRef.current.addTransceiver(audioTrack, { - direction: "sendrecv", - streams: [localStream] + direction: "sendrecv", + streams: [localStream] }); await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); - await attachReceiverE2EE(tx.receiver, Buffer.from(sharedSecret, "hex")); /** * Отправляем свой оффер другой стороне */ @@ -365,7 +369,7 @@ export function CallProvider(props : CallProviderProps) { send(offerSignal); return; } - }, [activeCall, sessionKeys]); + }, [activeCall, sessionKeys, sharedSecret]); const openCallsModal = (text : string) => { modals.open({ From 46af6661a19422a2111d64faa2f9fac7eda5e304 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:22:49 +0200 Subject: [PATCH 5/7] =?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 | 21 +++++++++++---------- app/providers/CallProvider/audioE2EE.ts | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 62996bc..c0ee1d3 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -74,7 +74,8 @@ export function CallProvider(props : CallProviderProps) { const roomIdRef = useRef(""); const roleRef = useRef(null); - const [sharedSecret, setSharedSecret] = useState(""); + //const [sharedSecret, setSharedSecret] = useState(""); + const sharedSecretRef = useRef(""); const iceServersRef = useRef([]); const remoteAudioRef = useRef(null); const iceCandidatesBufferRef = useRef([]); @@ -246,8 +247,8 @@ export function CallProvider(props : CallProviderProps) { } const sessionKeys = generateSessionKeys(); const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); - setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); - info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); + sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex'); + info("Generated shared secret for call session: " + sharedSecretRef.current); /** * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию */ @@ -284,8 +285,8 @@ export function CallProvider(props : CallProviderProps) { return; } const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); - info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); - setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); + sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex'); + info("Generated shared secret for call session: " + sharedSecretRef.current); setCallState(CallState.WEB_RTC_EXCHANGE); } if(signalType == SignalType.CREATE_ROOM) { @@ -326,7 +327,7 @@ export function CallProvider(props : CallProviderProps) { peerConnectionRef.current.ontrack = async (event) => { try { - await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecret, "hex")); + await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecretRef.current, "hex")); } catch (e) { console.error("attachReceiverE2EE failed:", e); } @@ -356,7 +357,7 @@ export function CallProvider(props : CallProviderProps) { streams: [localStream] }); - await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); + await attachSenderE2EE(tx.sender, Buffer.from(sharedSecretRef.current, "hex")); /** * Отправляем свой оффер другой стороне @@ -369,7 +370,7 @@ export function CallProvider(props : CallProviderProps) { send(offerSignal); return; } - }, [activeCall, sessionKeys, sharedSecret]); + }, [activeCall, sessionKeys]); const openCallsModal = (text : string) => { modals.open({ @@ -483,10 +484,10 @@ export function CallProvider(props : CallProviderProps) { * @returns */ const getKeyCast = () => { - if(!sharedSecret){ + if(!sharedSecretRef.current){ return ""; } - return sharedSecret; + return sharedSecretRef.current; } diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 0d385ba..97fbd6e 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -7,6 +7,7 @@ type KeyInput = Buffer | Uint8Array; async function importAesCtrKey(input: KeyInput): Promise { +console.info("Importing AES-CTR key for E2EE:", Buffer.from(input).toString('hex')); const keyBytes = toArrayBuffer(input); if (keyBytes.byteLength !== 32) { throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); From f91392e6aa78befa859f1cddcb4c8b85921c439f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:30:07 +0200 Subject: [PATCH 6/7] =?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=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20E2E?= =?UTF-8?q?E=20=D0=B8=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=20=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B0=D0=BC=D0=B8=20=D1=81=20DH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 141 ++++++++++++------------ 1 file changed, 70 insertions(+), 71 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 97fbd6e..216d065 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,31 +1,30 @@ 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; + const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); + return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; } type KeyInput = Buffer | Uint8Array; async function importAesCtrKey(input: KeyInput): Promise { -console.info("Importing AES-CTR key for E2EE:", Buffer.from(input).toString('hex')); - const keyBytes = toArrayBuffer(input); - if (keyBytes.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); - } + 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"] - ); + return crypto.subtle.importKey( + "raw", + keyBytes, + { name: "AES-CTR" }, + false, + ["encrypt", "decrypt"] + ); } function toBigIntTs(ts: unknown): bigint { - if (typeof ts === "bigint") return ts; - if (typeof ts === "number") return BigInt(ts); - return 0n; + if (typeof ts === "bigint") return ts; + if (typeof ts === "number") return BigInt(ts); + return 0n; } /** @@ -35,72 +34,72 @@ function toBigIntTs(ts: unknown): bigint { * [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); + 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 key = await importAesCtrKey(keyInput); - const anySender = sender as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const anySender = sender as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; + }; - if (!anySender.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpSender"); - } - - 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); + if (!anySender.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpSender"); } - }); - readable.pipeThrough(enc).pipeTo(writable).catch((e) => { - console.error("Sender E2EE pipeline failed:", e); - }); + 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); + } + }); + + 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 key = await importAesCtrKey(keyInput); - const anyReceiver = receiver as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const anyReceiver = receiver as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; + }; - if (!anyReceiver.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); - } - - 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); + if (!anyReceiver.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); } - }); - readable.pipeThrough(dec).pipeTo(writable).catch((e) => { - console.error("Receiver E2EE pipeline failed:", e); - }); + 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); + } + }); + + readable.pipeThrough(dec).pipeTo(writable).catch((e) => { + console.error("Receiver E2EE pipeline failed:", e); + }); } \ No newline at end of file From e5a4c92ba7cb0653e9b6e10103bf7e334d0c794f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:48:11 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 15 +++++---------- package.json | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/version.ts b/app/version.ts index 92dfb77..1bf51b8 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,13 +1,8 @@ -export const APP_VERSION = "1.1.0"; -export const CORE_MIN_REQUIRED_VERSION = "1.5.2"; +export const APP_VERSION = "1.1.1"; +export const CORE_MIN_REQUIRED_VERSION = "1.5.3"; export const RELEASE_NOTICE = ` -**Обновление v1.1.0** :emoji_1f631: -- Добавлена поддержка звонков -- Прозрачным аватаркам добавлена подложка -- Фикс ошибки чтения -- Подложка к вложению аватарки -- Обмен ключами шифрования DH -- Поддерджка WebRTC -- Событийные звуки звонка (сбросить, мутинг, и прочее...) +**Обновление v1.1.1** :emoji_1f631: +- Добавлено сквозное шифрование звонков +- Исправлена проблема с звуком в звонках на некоторых устройствах `; \ No newline at end of file diff --git a/package.json b/package.json index 985f4ec..9332cd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rosetta", - "version": "1.5.2", + "version": "1.5.3", "description": "Rosetta Messenger", "main": "./out/main/main.js", "license": "MIT",