Шифрование тест
This commit is contained in:
@@ -12,7 +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";
|
import { attachReceiverE2EE, attachSenderE2EE } from "./audioE2EE";
|
||||||
|
|
||||||
export interface CallContextValue {
|
export interface CallContextValue {
|
||||||
call: (callable: string) => void;
|
call: (callable: string) => void;
|
||||||
@@ -298,7 +298,9 @@ export function CallProvider(props : CallProviderProps) {
|
|||||||
* Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение
|
* Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение
|
||||||
*/
|
*/
|
||||||
peerConnectionRef.current = new RTCPeerConnection({
|
peerConnectionRef.current = new RTCPeerConnection({
|
||||||
iceServers: iceServersRef.current
|
iceServers: iceServersRef.current,
|
||||||
|
// @ts-ignore
|
||||||
|
encodedInsertableStreams: true
|
||||||
});
|
});
|
||||||
/**
|
/**
|
||||||
* Подписываемся на ICE кандидат
|
* Подписываемся на ICE кандидат
|
||||||
@@ -340,14 +342,18 @@ export function CallProvider(props : CallProviderProps) {
|
|||||||
* Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести,
|
* Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести,
|
||||||
* когда мы установим WebRTC соединение
|
* когда мы установим WebRTC соединение
|
||||||
*/
|
*/
|
||||||
const localStream = await navigator.mediaDevices.getUserMedia({
|
const localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
audio: true
|
const audioTrack = localStream.getAudioTracks()[0];
|
||||||
});
|
|
||||||
localStream.getTracks().forEach(track => {
|
|
||||||
peerConnectionRef.current?.addTrack(track, localStream);
|
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"));
|
||||||
/**
|
/**
|
||||||
* Отправляем свой оффер другой стороне
|
* Отправляем свой оффер другой стороне
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,74 +3,103 @@ function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer {
|
|||||||
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;
|
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enableAudioE2EE(pc: RTCPeerConnection, keyBuffer: Buffer | Uint8Array) {
|
type KeyInput = 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(
|
|
||||||
|
async function importAesCtrKey(input: KeyInput): Promise<CryptoKey> {
|
||||||
|
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",
|
||||||
raw,
|
keyBytes,
|
||||||
{ name: "AES-CTR" },
|
{ name: "AES-CTR" },
|
||||||
false,
|
false,
|
||||||
["encrypt", "decrypt"]
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const makeCounter = (ts: number, dir: number) => {
|
function toBigIntTs(ts: unknown): bigint {
|
||||||
const counter = new Uint8Array(16);
|
if (typeof ts === "bigint") return ts;
|
||||||
const dv = new DataView(counter.buffer);
|
if (typeof ts === "number") return BigInt(ts);
|
||||||
dv.setUint32(0, dir, false); // разделяем направление
|
return 0n;
|
||||||
dv.setBigUint64(8, BigInt(ts), false); // nonce из timestamp кадра
|
}
|
||||||
return counter;
|
|
||||||
|
/**
|
||||||
|
* 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<void> {
|
||||||
|
const key = await importAesCtrKey(keyInput);
|
||||||
|
|
||||||
|
const anySender = sender as unknown as {
|
||||||
|
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachSender = (sender: RTCRtpSender) => {
|
if (!anySender.createEncodedStreams) {
|
||||||
// @ts-ignore Chromium/Electron API
|
throw new Error("createEncodedStreams is not available on RTCRtpSender");
|
||||||
if (!sender.createEncodedStreams) return;
|
}
|
||||||
// @ts-ignore
|
|
||||||
const { readable, writable } = sender.createEncodedStreams();
|
|
||||||
|
|
||||||
const enc = new TransformStream({
|
const { readable, writable } = anySender.createEncodedStreams();
|
||||||
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 enc = new TransformStream<any, any>({
|
||||||
|
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<void> {
|
||||||
|
const key = await importAesCtrKey(keyInput);
|
||||||
|
|
||||||
|
const anyReceiver = receiver as unknown as {
|
||||||
|
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachReceiver = (receiver: RTCRtpReceiver) => {
|
if (!anyReceiver.createEncodedStreams) {
|
||||||
// @ts-ignore Chromium/Electron API
|
throw new Error("createEncodedStreams is not available on RTCRtpReceiver");
|
||||||
if (!receiver.createEncodedStreams) return;
|
}
|
||||||
// @ts-ignore
|
|
||||||
const { readable, writable } = receiver.createEncodedStreams();
|
|
||||||
|
|
||||||
const dec = new TransformStream({
|
const { readable, writable } = anyReceiver.createEncodedStreams();
|
||||||
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(() => {});
|
const dec = new TransformStream<any, any>({
|
||||||
};
|
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));
|
readable.pipeThrough(dec).pipeTo(writable).catch((e) => {
|
||||||
pc.getReceivers().forEach((r) => r.track?.kind === "audio" && attachReceiver(r));
|
console.error("Receiver E2EE pipeline failed:", e);
|
||||||
|
|
||||||
pc.addEventListener("track", (e) => {
|
|
||||||
if (e.track.kind === "audio") attachReceiver(e.receiver);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user