livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
145 lines (131 loc) • 4.85 kB
text/typescript
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,
);
}
}
}
}