WASM ускоренный алгоритм шифрования для избежания backpressure
This commit is contained in:
@@ -1,54 +1,46 @@
|
|||||||
function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer {
|
import { chacha20 } from "@noble/ciphers/chacha";
|
||||||
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;
|
type KeyInput = Buffer | Uint8Array;
|
||||||
|
|
||||||
|
const senderAttached = new WeakSet<RTCRtpSender>();
|
||||||
|
const receiverAttached = new WeakSet<RTCRtpReceiver>();
|
||||||
|
|
||||||
async function importAesCtrKey(input: KeyInput): Promise<CryptoKey> {
|
function toUint8Array(input: KeyInput): Uint8Array {
|
||||||
const keyBytes = toArrayBuffer(input);
|
const u8 = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
if (keyBytes.byteLength !== 32) {
|
return new Uint8Array(u8.slice().buffer);
|
||||||
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 toBigIntTs(ts: unknown): bigint {
|
function buildNonce(timestamp: unknown): Uint8Array {
|
||||||
if (typeof ts === "bigint") return ts;
|
const nonce = new Uint8Array(12);
|
||||||
if (typeof ts === "number") return BigInt(ts);
|
const ts = typeof timestamp === "number"
|
||||||
return 0n;
|
? timestamp
|
||||||
|
: typeof timestamp === "bigint"
|
||||||
|
? Number(timestamp)
|
||||||
|
: 0;
|
||||||
|
new DataView(nonce.buffer).setUint32(8, ts >>> 0, false);
|
||||||
|
return nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer {
|
||||||
* 16-byte counter:
|
const nonce = buildNonce(timestamp);
|
||||||
* [0..3] direction marker
|
const input = new Uint8Array(data);
|
||||||
* [4..11] frame timestamp
|
// ChaCha20 симметричный: encrypt === decrypt, тот же размер
|
||||||
* [12..15] reserved
|
const output = chacha20(key, nonce, input);
|
||||||
*/
|
return output.buffer as ArrayBuffer;
|
||||||
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> {
|
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 {
|
const key = toUint8Array(keyInput);
|
||||||
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
|
if (key.byteLength !== 32) {
|
||||||
};
|
throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const anySender = sender as any;
|
||||||
if (!anySender.createEncodedStreams) {
|
if (!anySender.createEncodedStreams) {
|
||||||
throw new Error("createEncodedStreams is not available on RTCRtpSender");
|
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 { readable, writable } = anySender.createEncodedStreams();
|
||||||
|
|
||||||
const enc = new TransformStream<any, any>({
|
const enc = new TransformStream<any, any>({
|
||||||
async transform(frame, controller) {
|
// Синхронный transform — нет async, нет накопления очереди
|
||||||
const counter = buildCounter(1, frame.timestamp);
|
transform(frame, controller) {
|
||||||
const encrypted = await crypto.subtle.encrypt(
|
try {
|
||||||
{ name: "AES-CTR", counter, length: 64 },
|
frame.data = processFrame(frame.data, key, frame.timestamp);
|
||||||
key,
|
controller.enqueue(frame);
|
||||||
frame.data
|
} catch (e) {
|
||||||
);
|
console.error("Sender E2EE frame failed:", e);
|
||||||
frame.data = encrypted; // same length
|
controller.enqueue(frame);
|
||||||
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> {
|
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 {
|
const key = toUint8Array(keyInput);
|
||||||
createEncodedStreams?: () => { readable: ReadableStream<any>; writable: WritableStream<any> };
|
if (key.byteLength !== 32) {
|
||||||
};
|
throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const anyReceiver = receiver as any;
|
||||||
if (!anyReceiver.createEncodedStreams) {
|
if (!anyReceiver.createEncodedStreams) {
|
||||||
throw new Error("createEncodedStreams is not available on RTCRtpReceiver");
|
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 { readable, writable } = anyReceiver.createEncodedStreams();
|
||||||
|
|
||||||
const dec = new TransformStream<any, any>({
|
const dec = new TransformStream<any, any>({
|
||||||
async transform(frame, controller) {
|
// Синхронный transform — нет async, нет накопления очереди
|
||||||
const counter = buildCounter(1, frame.timestamp);
|
transform(frame, controller) {
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
try {
|
||||||
{ name: "AES-CTR", counter, length: 64 },
|
frame.data = processFrame(frame.data, key, frame.timestamp);
|
||||||
key,
|
controller.enqueue(frame);
|
||||||
frame.data
|
} catch (e) {
|
||||||
);
|
console.error("Receiver E2EE frame failed:", e);
|
||||||
frame.data = decrypted; // same length
|
controller.enqueue(frame);
|
||||||
controller.enqueue(frame);
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"electronUpdaterCompatibility": false,
|
"electronUpdaterCompatibility": false,
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{ "from": "resources/", "to": "resources/" }
|
{
|
||||||
|
"from": "resources/",
|
||||||
|
"to": "resources/"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"node_modules/sqlite3/**/*",
|
"node_modules/sqlite3/**/*",
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
"@mantine/form": "^8.3.12",
|
"@mantine/form": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.12",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"@mantine/modals": "^8.3.12",
|
"@mantine/modals": "^8.3.12",
|
||||||
"@noble/ciphers": "^1.2.1",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@noble/secp256k1": "^3.0.0",
|
"@noble/secp256k1": "^3.0.0",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user