@simplito/privmx-webendpoint
Version:
PrivMX Web Endpoint library
205 lines (204 loc) • 8.46 kB
JavaScript
"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");