WASM ускоренный алгоритм шифрования для избежания backpressure

This commit is contained in:
RoyceDa
2026-03-20 21:24:36 +02:00
parent e5a4c92ba7
commit 5032d92f8e
2 changed files with 62 additions and 62 deletions

View File

@@ -1,54 +1,46 @@
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;
}
import { chacha20 } from "@noble/ciphers/chacha";
type KeyInput = Buffer | Uint8Array;
const senderAttached = new WeakSet<RTCRtpSender>();
const receiverAttached = new WeakSet<RTCRtpReceiver>();
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",
keyBytes,
{ name: "AES-CTR" },
false,
["encrypt", "decrypt"]
);
function toUint8Array(input: KeyInput): Uint8Array {
const u8 = input instanceof Uint8Array ? input : new Uint8Array(input);
return new Uint8Array(u8.slice().buffer);
}
function toBigIntTs(ts: unknown): bigint {
if (typeof ts === "bigint") return ts;
if (typeof ts === "number") return BigInt(ts);
return 0n;
function buildNonce(timestamp: unknown): Uint8Array {
const nonce = new Uint8Array(12);
const ts = typeof timestamp === "number"
? timestamp
: typeof timestamp === "bigint"
? Number(timestamp)
: 0;
new DataView(nonce.buffer).setUint32(8, ts >>> 0, false);
return nonce;
}
/**
* 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);
function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer {
const nonce = buildNonce(timestamp);
const input = new Uint8Array(data);
// ChaCha20 симметричный: encrypt === decrypt, тот же размер
const output = chacha20(key, nonce, input);
return output.buffer as ArrayBuffer;
}
export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise<void> {
const key = await importAesCtrKey(keyInput);
if (senderAttached.has(sender)) {
return;
}
senderAttached.add(sender);
const anySender = sender as unknown as {
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
};
const key = toUint8Array(keyInput);
if (key.byteLength !== 32) {
throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`);
}
const anySender = sender as any;
if (!anySender.createEncodedStreams) {
throw new Error("createEncodedStreams is not available on RTCRtpSender");
}
@@ -56,15 +48,15 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput)
const { readable, writable } = anySender.createEncodedStreams();
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);
// Синхронный transform — нет async, нет накопления очереди
transform(frame, controller) {
try {
frame.data = processFrame(frame.data, key, frame.timestamp);
controller.enqueue(frame);
} catch (e) {
console.error("Sender E2EE frame failed:", e);
controller.enqueue(frame);
}
}
});
@@ -74,12 +66,17 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput)
}
export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise<void> {
const key = await importAesCtrKey(keyInput);
if (receiverAttached.has(receiver)) {
return;
}
receiverAttached.add(receiver);
const anyReceiver = receiver as unknown as {
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
};
const key = toUint8Array(keyInput);
if (key.byteLength !== 32) {
throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`);
}
const anyReceiver = receiver as any;
if (!anyReceiver.createEncodedStreams) {
throw new Error("createEncodedStreams is not available on RTCRtpReceiver");
}
@@ -87,15 +84,15 @@ export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: Key
const { readable, writable } = anyReceiver.createEncodedStreams();
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);
// Синхронный transform — нет async, нет накопления очереди
transform(frame, controller) {
try {
frame.data = processFrame(frame.data, key, frame.timestamp);
controller.enqueue(frame);
} catch (e) {
console.error("Receiver E2EE frame failed:", e);
controller.enqueue(frame);
}
}
});