UNPKG

@simplito/privmx-webendpoint

Version:

PrivMX Web Endpoint library

205 lines (204 loc) 8.46 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EncryptTransform = void 0; const Utils_1 = require("../Utils"); const CryptoUtils_1 = require("../CryptoUtils"); const KeyStore_1 = require("../KeyStore"); const LocalAudioLevelMeter_1 = require("../audio/LocalAudioLevelMeter"); const NUM_AS_UINT8_SIZE = 1; const DEBUG = false; const sessions = new Map(); const pipelines = new Map(); let lastRMS = LocalAudioLevelMeter_1.LocalAudioLevelMeter.RMS_VALUE_OF_SILENCE; let recvRMS = LocalAudioLevelMeter_1.LocalAudioLevelMeter.RMS_VALUE_OF_SILENCE; let recvRMSTimestamp = Date.now(); class EncryptTransform { keyStore; constructor(keyStore) { this.keyStore = keyStore; } getHeaderSizeByType(type) { if (type === "key") return 10; if (type === "delta") return 3; if (type === "empty") return 1; return 0; } async encryptFrame(encodedFrame, kind, controller) { const headerLen = kind === "video" ? this.getHeaderSizeByType(encodedFrame.type) : 1; const frameHeader = new Uint8Array(encodedFrame.data, 0, headerLen); const frameBody = new Uint8Array(encodedFrame.data, headerLen); const iv = Utils_1.Utils.genIvAsBuffer(); const keyId = this.keyStore.getEncryptionKeyId(); const cryptoKey = await this.keyStore.getEncriptionKey(); const cryptoResult = await (0, CryptoUtils_1.encryptWithAES256GCM)(cryptoKey, iv, frameBody, frameHeader); if (!(0, CryptoUtils_1.isEncryptionSuccess)(cryptoResult)) { throw new Error("Cannot encrypt frame"); } const keyIdAsUint8 = new TextEncoder().encode(keyId); const posOfCipher = frameHeader.byteLength; const posOfIv = posOfCipher + cryptoResult.data.byteLength; const posOfIvSize = posOfIv + iv.byteLength; const posOfKeyId = posOfIvSize + NUM_AS_UINT8_SIZE; const posOfKeyIdSize = posOfKeyId + keyIdAsUint8.byteLength; const posOfRMS = posOfKeyIdSize + NUM_AS_UINT8_SIZE; const result = new ArrayBuffer(posOfRMS + NUM_AS_UINT8_SIZE); const resultUint8 = new Uint8Array(result); resultUint8.set(frameHeader); resultUint8.set(new Uint8Array(cryptoResult.data), posOfCipher); resultUint8.set(iv, posOfIv); resultUint8.set(Utils_1.Utils.numAsOneByteUint(iv.byteLength), posOfIvSize); resultUint8.set(keyIdAsUint8, posOfKeyId); resultUint8.set(Utils_1.Utils.numAsOneByteUint(keyIdAsUint8.byteLength), posOfKeyIdSize); resultUint8.set(Utils_1.Utils.numAsOneByteUint(lastRMS + 100), posOfRMS); encodedFrame.data = result; controller.enqueue(encodedFrame); } async decryptFrame(encodedFrame, kind, controller, receiverId, publisherId) { const headerLen = kind === "video" ? this.getHeaderSizeByType(encodedFrame.type) : 1; const data = encodedFrame.data; if (data.byteLength < headerLen + 5) { // Sanity check for minimum metadata size controller.enqueue(encodedFrame); return; } const frameHeader = new Uint8Array(data, 0, headerLen); const rmsPos = data.byteLength - 1; recvRMS = new Uint8Array(data, rmsPos, 1)[0] - 100; const currTime = Date.now(); if (recvRMSTimestamp + 100 < currTime) { recvRMSTimestamp = currTime; self.postMessage({ type: "rms", rms: recvRMS, receiverId, publisherId }); } const keyIdLenPos = rmsPos - 1; const keyIdLen = new Uint8Array(data, keyIdLenPos, 1)[0]; const keyIdPos = keyIdLenPos - keyIdLen; const keyId = new TextDecoder().decode(new Uint8Array(data, keyIdPos, keyIdLen)); const ivLenPos = keyIdPos - 1; const ivLen = new Uint8Array(data, ivLenPos, 1)[0]; const ivPos = ivLenPos - ivLen; const iv = new Uint8Array(data, ivPos, ivLen); const payloadPos = headerLen; const payloadLen = ivPos - headerLen; const payload = data.slice(payloadPos, payloadPos + payloadLen); try { if (!this.keyStore.hasKey(keyId)) { controller.enqueue(encodedFrame); return; } const cryptoKey = await this.keyStore.getKey(keyId); const decryptionResult = await (0, CryptoUtils_1.decryptWithAES256GCM)(cryptoKey, iv, payload, frameHeader); if (!(0, CryptoUtils_1.isDecryptionSuccess)(decryptionResult)) { controller.enqueue(encodedFrame); return; } const plain = decryptionResult.data; const result = new ArrayBuffer(frameHeader.byteLength + plain.byteLength); const writableResult = new Uint8Array(result); writableResult.set(frameHeader); writableResult.set(new Uint8Array(plain), frameHeader.byteLength); encodedFrame.data = result; controller.enqueue(encodedFrame); } catch (e) { logError(e); controller.enqueue(encodedFrame); } } } exports.EncryptTransform = EncryptTransform; self.keyStore = new KeyStore_1.KeyStore(); const getKeyStore = () => self.keyStore; self.onmessage = async (event) => { const { operation, kind } = event.data; if (operation === "initialize") { logDebug("worker initialize call"); } else if (operation === "init-pipeline") { pipelines.set(event.data.id, { ready: false }); self.postMessage({ operation: "init-pipeline", id: event.data.id }); } else if (operation === "encode" || operation === "decode") { const { readableStream, writableStream, id, publisherId } = event.data; const context = { keyStore: getKeyStore(), id, publisherId }; handleTransform(context, operation, kind, readableStream, writableStream); } else if (operation === "setKeys") { const data = event.data; getKeyStore().setKeys(data.keys); } else if (operation === "rms") { lastRMS = Math.round(event.data.rms); } }; function createSenderTransform(keyStore, kind) { const encrypter = new EncryptTransform(keyStore); return new TransformStream({ async transform(encodedFrame, controller) { await encrypter.encryptFrame(encodedFrame, kind, controller); }, }); } function createReceiverTransform(context, kind) { const encrypter = new EncryptTransform(context.keyStore); return new TransformStream({ async transform(encodedFrame, controller) { await encrypter.decryptFrame(encodedFrame, kind, controller, context.id, context.publisherId); }, }); } function handleTransform(context, operation, kind, readableStream, writableStream) { let transformStream; logDebug("handleTransform: " + JSON.stringify({ operation, context })); if (operation === "encode") { transformStream = createSenderTransform(context.keyStore, kind); readableStream.pipeThrough(transformStream).pipeTo(writableStream); } else if (operation === "decode") { transformStream = createReceiverTransform(context, kind); const pipeline = readableStream .pipeThrough(transformStream) .pipeTo(writableStream) .catch((err) => { if (!String(err).includes("Destination stream closed")) { console.error("pipeline error", err); } }); if (context.id) { sessions.set(context.id, { pipeline }); } } } if (self.RTCTransformEvent) { self.onrtctransform = (event) => { const transformer = event.transformer; const options = transformer.options; if (!options) { logError("onrtctransform: options is undefined"); return; } const { operation, kind, id, publisherId } = options; const context = { keyStore: getKeyStore(), id, publisherId, }; handleTransform(context, operation, kind, transformer.readable, transformer.writable); }; } /** * LOGGING UTILS */ function logDebug(msg) { if (!DEBUG) return; self.postMessage({ type: "debug", data: msg }); } function logError(msg) { self.postMessage({ type: "error", data: msg }); } logDebug("Worker Initialized");