Files
desktop/app/providers/CallProvider/audioE2EE.ts

130 lines
4.4 KiB
TypeScript

import _sodium from "libsodium-wrappers";
type KeyInput = Buffer | Uint8Array;
const senderAttached = new WeakSet<RTCRtpSender>();
const receiverAttached = new WeakSet<RTCRtpReceiver>();
let sodiumReady = false;
let sodium: typeof _sodium;
export async function initE2EE(): Promise<void> {
if (sodiumReady) return;
await _sodium.ready;
sodium = _sodium;
sodiumReady = true;
}
function toUint8Array(input: KeyInput): Uint8Array {
const u8 = input instanceof Uint8Array ? input : new Uint8Array(input);
return new Uint8Array(u8.slice().buffer);
}
function createFrameProcessor(key: Uint8Array) {
// Переиспользуемый nonce буфер — нет аллокаций на каждый фрейм
const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes
return function processFrame(data: ArrayBuffer): ArrayBuffer {
const input = new Uint8Array(data);
// Обновляем nonce из длины и byteOffset фрейма — уникально и без аллокаций
nonce.fill(0);
new DataView(nonce.buffer).setUint32(0, input.byteLength ^ 0xdeadbeef, false);
new DataView(nonce.buffer).setUint32(4, input[0] ^ input[input.byteLength - 1], false);
// WASM ChaCha20 — синхронный, нативная скорость
const output = sodium.crypto_stream_chacha20_xor(input, nonce, key);
return output.buffer as ArrayBuffer;
};
}
function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) {
let frames = 0;
let slowFrames = 0;
let total = 0;
let max = 0;
return new TransformStream<any, any>({
transform(frame, controller) {
const started = performance.now();
try {
frame.data = processFrame(frame.data);
} catch (e) {
console.error("[E2EE] frame error:", e);
controller.enqueue(frame);
return;
}
const elapsed = performance.now() - started;
frames++;
total += elapsed;
if (elapsed > max) max = elapsed;
if (elapsed > 2) slowFrames++;
if (frames >= 200) {
console.info("[E2EE stats]", {
avgMs: +(total / frames).toFixed(3),
maxMs: +max.toFixed(3),
slowFrames,
});
frames = 0;
slowFrames = 0;
total = 0;
max = 0;
}
controller.enqueue(frame);
}
});
}
export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise<void> {
if (senderAttached.has(sender)) return;
senderAttached.add(sender);
await initE2EE();
const key = toUint8Array(keyInput);
if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) {
throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`);
}
const anySender = sender as any;
if (!anySender.createEncodedStreams) {
throw new Error("createEncodedStreams not available on RTCRtpSender");
}
const { readable, writable } = anySender.createEncodedStreams();
const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
readable
.pipeThrough(createTransform(processFrame))
.pipeTo(writable)
.catch((e) => console.error("[E2EE] Sender pipeline failed:", e));
}
export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise<void> {
if (receiverAttached.has(receiver)) return;
receiverAttached.add(receiver);
await initE2EE();
const key = toUint8Array(keyInput);
if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) {
throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`);
}
const anyReceiver = receiver as any;
if (!anyReceiver.createEncodedStreams) {
throw new Error("createEncodedStreams not available on RTCRtpReceiver");
}
const { readable, writable } = anyReceiver.createEncodedStreams();
const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
readable
.pipeThrough(createTransform(processFrame))
.pipeTo(writable)
.catch((e) => console.error("[E2EE] Receiver pipeline failed:", e));
}