UNPKG

livekit-client

Version:

JavaScript/TypeScript client SDK for LiveKit

145 lines (131 loc) 4.85 kB
import { workerLogger } from '../../logger'; import { ENCRYPTION_ALGORITHM } from '../constants'; import { CryptorError, CryptorErrorReason } from '../errors'; import type { DecodeRatchetOptions, KeySet, RatchetResult } from '../types'; import { deriveKeys } from '../utils'; import type { ParticipantKeyHandler } from './ParticipantKeyHandler'; export class DataCryptor { private static sendCount = 0; private static makeIV(timestamp: number) { const iv = new ArrayBuffer(12); const ivView = new DataView(iv); const randomBytes = crypto.getRandomValues(new Uint32Array(1)); ivView.setUint32(0, randomBytes[0]); ivView.setUint32(4, timestamp); ivView.setUint32(8, timestamp - (DataCryptor.sendCount % 0xffff)); DataCryptor.sendCount++; return iv; } static async encrypt( data: Uint8Array, keys: ParticipantKeyHandler, ): Promise<{ payload: Uint8Array; iv: Uint8Array; keyIndex: number; }> { const iv = DataCryptor.makeIV(performance.now()); const keySet = await keys.getKeySet(); if (!keySet) { throw new Error('No key set found'); } const cipherText = await crypto.subtle.encrypt( { name: ENCRYPTION_ALGORITHM, iv, }, keySet.encryptionKey, new Uint8Array(data), ); return { payload: new Uint8Array(cipherText), iv: new Uint8Array(iv), keyIndex: keys.getCurrentKeyIndex(), }; } static async decrypt( data: Uint8Array, iv: Uint8Array, keys: ParticipantKeyHandler, keyIndex: number = 0, initialMaterial?: KeySet, ratchetOpts: DecodeRatchetOptions = { ratchetCount: 0 }, ): Promise<{ payload: Uint8Array; }> { const keySet = await keys.getKeySet(keyIndex); if (!keySet) { throw new Error('No key set found'); } try { const plainText = await crypto.subtle.decrypt( { name: ENCRYPTION_ALGORITHM, iv, }, keySet.encryptionKey, new Uint8Array(data), ); return { payload: new Uint8Array(plainText), }; } catch (error: any) { if (keys.keyProviderOptions.ratchetWindowSize > 0) { if (ratchetOpts.ratchetCount < keys.keyProviderOptions.ratchetWindowSize) { workerLogger.debug( `DataCryptor: ratcheting key attempt ${ratchetOpts.ratchetCount} of ${ keys.keyProviderOptions.ratchetWindowSize }, for data packet`, ); let ratchetedKeySet: KeySet | undefined; let ratchetResult: RatchetResult | undefined; if ((initialMaterial ?? keySet) === keys.getKeySet(keyIndex)) { // only ratchet if the currently set key is still the same as the one used to decrypt this frame // if not, it might be that a different frame has already ratcheted and we try with that one first ratchetResult = await keys.ratchetKey(keyIndex, false); ratchetedKeySet = await deriveKeys(ratchetResult.cryptoKey, keys.keyProviderOptions); } const decryptedData = await DataCryptor.decrypt( data, iv, keys, keyIndex, initialMaterial, { ratchetCount: ratchetOpts.ratchetCount + 1, encryptionKey: ratchetedKeySet?.encryptionKey, }, ); if (decryptedData && ratchetedKeySet) { // before updating the keys, make sure that the keySet used for this frame is still the same as the currently set key // if it's not, a new key might have been set already, which we don't want to override if ((initialMaterial ?? keySet) === keys.getKeySet(keyIndex)) { keys.setKeySet(ratchetedKeySet, keyIndex, ratchetResult); // decryption was successful, set the new key index to reflect the ratcheted key set keys.setCurrentKeyIndex(keyIndex); } } return decryptedData; } else { /** * Because we only set a new key once decryption has been successful, * we can be sure that we don't need to reset the key to the initial material at this point * as the key has not been updated on the keyHandler instance */ workerLogger.warn('DataCryptor: maximum ratchet attempts exceeded'); throw new CryptorError( `DataCryptor: valid key missing for participant ${keys.participantIdentity}`, CryptorErrorReason.InvalidKey, keys.participantIdentity, ); } } else { throw new CryptorError( `DataCryptor: Decryption failed: ${error.message}`, CryptorErrorReason.InvalidKey, keys.participantIdentity, ); } } } }