UNPKG

livekit-client

Version:

JavaScript/TypeScript client SDK for LiveKit

196 lines (173 loc) 5.59 kB
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; }