You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

148 lines
5.5 KiB

3 months ago
/*
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree.
*/
/*
* This is a worker doing the encode/decode transformations to add end-to-end
* encryption to a WebRTC PeerConnection using the Insertable Streams API.
*/
'use strict';
let currentCryptoKey;
let useCryptoOffset = true;
let currentKeyIdentifier = 0;
// If using crypto offset (controlled by a checkbox):
// Do not encrypt the first couple of bytes of the payload. This allows
// a middle to determine video keyframes or the opus mode being used.
// For VP8 this is the content described in
// https://tools.ietf.org/html/rfc6386#section-9.1
// which is 10 bytes for key frames and 3 bytes for delta frames.
// For opus (where encodedFrame.type is not set) this is the TOC byte from
// https://tools.ietf.org/html/rfc6716#section-3.1
//
// It makes the (encrypted) video and audio much more fun to watch and listen to
// as the decoder does not immediately throw a fatal error.
const frameTypeToCryptoOffset = {
key: 10,
delta: 3,
undefined: 1,
};
function dump(encodedFrame, direction, max = 16) {
const data = new Uint8Array(encodedFrame.data);
let bytes = '';
for (let j = 0; j < data.length && j < max; j++) {
bytes += (data[j] < 16 ? '0' : '') + data[j].toString(16) + ' ';
}
console.log('[e2e worker]', performance.now().toFixed(2), direction, bytes.trim(),
'len=' + encodedFrame.data.byteLength,
'type=' + (encodedFrame.type || 'audio'),
'ts=' + encodedFrame.timestamp,
'ssrc=' + encodedFrame.getMetadata().synchronizationSource
);
}
let scount = 0;
function encodeFunction(encodedFrame, controller) {
if (scount++ < 30) { // dump the first 30 packets.
dump(encodedFrame, 'send');
}
if (currentCryptoKey) {
const view = new DataView(encodedFrame.data);
// Any length that is needed can be used for the new buffer.
const newData = new ArrayBuffer(encodedFrame.data.byteLength + 5);
const newView = new DataView(newData);
const cryptoOffset = useCryptoOffset? frameTypeToCryptoOffset[encodedFrame.type] : 0;
for (let i = 0; i < cryptoOffset && i < encodedFrame.data.byteLength; ++i) {
newView.setInt8(i, view.getInt8(i));
}
// This is a bitwise xor of the key with the payload. This is not strong encryption, just a demo.
for (let i = cryptoOffset; i < encodedFrame.data.byteLength; ++i) {
const keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length);
newView.setInt8(i, view.getInt8(i) ^ keyByte);
}
// Append keyIdentifier.
newView.setUint8(encodedFrame.data.byteLength, currentKeyIdentifier % 0xff);
// Append checksum
newView.setUint32(encodedFrame.data.byteLength + 1, 0xDEADBEEF);
encodedFrame.data = newData;
}
controller.enqueue(encodedFrame);
}
let rcount = 0;
function decodeFunction(encodedFrame, controller) {
if (rcount++ < 30) { // dump the first 30 packets
dump(encodedFrame, 'recv');
}
const view = new DataView(encodedFrame.data);
const checksum = encodedFrame.data.byteLength > 4 ? view.getUint32(encodedFrame.data.byteLength - 4) : false;
if (currentCryptoKey) {
if (checksum !== 0xDEADBEEF) {
console.log('Corrupted frame received, checksum ' +
checksum.toString(16));
return; // This can happen when the key is set and there is an unencrypted frame in-flight.
}
const keyIdentifier = view.getUint8(encodedFrame.data.byteLength - 5);
if (keyIdentifier !== currentKeyIdentifier) {
console.log(`Key identifier mismatch, got ${keyIdentifier} expected ${currentKeyIdentifier}.`);
return;
}
const newData = new ArrayBuffer(encodedFrame.data.byteLength - 5);
const newView = new DataView(newData);
const cryptoOffset = useCryptoOffset?
Math.min(newView.byteLength, frameTypeToCryptoOffset[encodedFrame.type])
: 0;
for (let i = 0; i < cryptoOffset; ++i) {
newView.setInt8(i, view.getInt8(i));
}
for (let i = cryptoOffset; i < encodedFrame.data.byteLength - 5; ++i) {
const keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length);
newView.setInt8(i, view.getInt8(i) ^ keyByte);
}
encodedFrame.data = newData;
} else if (checksum === 0xDEADBEEF) {
return; // encrypted in-flight frame but we already forgot about the key.
}
controller.enqueue(encodedFrame);
}
onmessage = async (event) => {
const { operation } = event.data;
if (operation === 'encode') {
console.log('[e2e worker]', operation);
const {readableStream, writableStream} = event.data;
const transformStream = new TransformStream({
transform: encodeFunction,
});
readableStream
.pipeThrough(transformStream)
.pipeTo(writableStream);
} else if (operation === 'decode') {
console.log('[e2e worker]', operation);
const {readableStream, writableStream} = event.data;
const transformStream = new TransformStream({
transform: decodeFunction,
});
readableStream
.pipeThrough(transformStream)
.pipeTo(writableStream);
} else if (operation === 'setCryptoKey') {
console.log('[e2e worker]', operation, event.data.currentCryptoKey);
if (event.data.currentCryptoKey !== currentCryptoKey) {
currentKeyIdentifier++;
}
currentCryptoKey = event.data.currentCryptoKey;
useCryptoOffset = event.data.useCryptoOffset;
}
};