livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
196 lines (173 loc) • 5.59 kB
text/typescript
import { type DataPacket, EncryptedPacketPayload } from '@livekit/protocol';
import { ENCRYPTION_ALGORITHM } from './constants';
import type { KeyProviderOptions } from './types';
export function isE2EESupported() {
return isInsertableStreamSupported() || isScriptTransformSupported();
}
export function isScriptTransformSupported() {
// @ts-ignore
return typeof window.RTCRtpScriptTransform !== 'undefined';
}
export function isInsertableStreamSupported() {
return (
typeof window.RTCRtpSender !== 'undefined' &&
// @ts-ignore
typeof window.RTCRtpSender.prototype.createEncodedStreams !== 'undefined'
);
}
export function isVideoFrame(
frame: RTCEncodedAudioFrame | RTCEncodedVideoFrame,
): frame is RTCEncodedVideoFrame {
return 'type' in frame;
}
export async function importKey(
keyBytes: Uint8Array | ArrayBuffer,
algorithm: string | { name: string } = { name: ENCRYPTION_ALGORITHM },
usage: 'derive' | 'encrypt' = 'encrypt',
) {
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
return crypto.subtle.importKey(
'raw',
keyBytes,
algorithm,
false,
usage === 'derive' ? ['deriveBits', 'deriveKey'] : ['encrypt', 'decrypt'],
);
}
export async function createKeyMaterialFromString(password: string) {
let enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
{
name: 'PBKDF2',
},
false,
['deriveBits', 'deriveKey'],
);
return keyMaterial;
}
export async function createKeyMaterialFromBuffer(cryptoBuffer: ArrayBuffer) {
const keyMaterial = await crypto.subtle.importKey('raw', cryptoBuffer, 'HKDF', false, [
'deriveBits',
'deriveKey',
]);
return keyMaterial;
}
function getAlgoOptions(algorithmName: string, salt: string) {
const textEncoder = new TextEncoder();
const encodedSalt = textEncoder.encode(salt);
switch (algorithmName) {
case 'HKDF':
return {
name: 'HKDF',
salt: encodedSalt,
hash: 'SHA-256',
info: new ArrayBuffer(128),
};
case 'PBKDF2': {
return {
name: 'PBKDF2',
salt: encodedSalt,
hash: 'SHA-256',
iterations: 100000,
};
}
default:
throw new Error(`algorithm ${algorithmName} is currently unsupported`);
}
}
/**
* Derives a set of keys from the master key.
* See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
*/
export async function deriveKeys(material: CryptoKey, options: KeyProviderOptions) {
const algorithmOptions = getAlgoOptions(material.algorithm.name, options.ratchetSalt);
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
// https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
const encryptionKey = await crypto.subtle.deriveKey(
algorithmOptions,
material,
{
name: ENCRYPTION_ALGORITHM,
length: options.keySize,
},
false,
['encrypt', 'decrypt'],
);
return { material, encryptionKey };
}
export function createE2EEKey(): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(32));
}
/**
* Ratchets a key. See
* https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
*/
export async function ratchet(material: CryptoKey, salt: string): Promise<ArrayBuffer> {
const algorithmOptions = getAlgoOptions(material.algorithm.name, salt);
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
return crypto.subtle.deriveBits(algorithmOptions, material, 256);
}
export function needsRbspUnescaping(frameData: Uint8Array) {
for (var i = 0; i < frameData.length - 3; i++) {
if (frameData[i] == 0 && frameData[i + 1] == 0 && frameData[i + 2] == 3) return true;
}
return false;
}
export function parseRbsp(stream: Uint8Array): Uint8Array {
const dataOut: number[] = [];
var length = stream.length;
for (var i = 0; i < stream.length; ) {
// Be careful about over/underflow here. byte_length_ - 3 can underflow, and
// i + 3 can overflow, but byte_length_ - i can't, because i < byte_length_
// above, and that expression will produce the number of bytes left in
// the stream including the byte at i.
if (length - i >= 3 && !stream[i] && !stream[i + 1] && stream[i + 2] == 3) {
// Two rbsp bytes.
dataOut.push(stream[i++]);
dataOut.push(stream[i++]);
// Skip the emulation byte.
i++;
} else {
// Single rbsp byte.
dataOut.push(stream[i++]);
}
}
return new Uint8Array(dataOut);
}
const kZerosInStartSequence = 2;
const kEmulationByte = 3;
export function writeRbsp(data_in: Uint8Array): Uint8Array {
const dataOut: number[] = [];
var numConsecutiveZeros = 0;
for (var i = 0; i < data_in.length; ++i) {
var byte = data_in[i];
if (byte <= kEmulationByte && numConsecutiveZeros >= kZerosInStartSequence) {
// Need to escape.
dataOut.push(kEmulationByte);
numConsecutiveZeros = 0;
}
dataOut.push(byte);
if (byte == 0) {
++numConsecutiveZeros;
} else {
numConsecutiveZeros = 0;
}
}
return new Uint8Array(dataOut);
}
export function asEncryptablePacket(packet: DataPacket): EncryptedPacketPayload | undefined {
if (
packet.value?.case !== 'sipDtmf' &&
packet.value?.case !== 'metrics' &&
packet.value?.case !== 'speaker' &&
packet.value?.case !== 'transcription' &&
packet.value?.case !== 'encryptedPacket'
) {
return new EncryptedPacketPayload({
value: packet.value,
});
}
return undefined;
}