livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
117 lines (103 loc) • 3.89 kB
text/typescript
import { EventEmitter } from 'events';
import type TypedEventEmitter from 'typed-emitter';
import log from '../logger';
import { KEY_PROVIDER_DEFAULTS } from './constants';
import { type KeyProviderCallbacks, KeyProviderEvent } from './events';
import type { KeyInfo, KeyProviderOptions, RatchetResult } from './types';
import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './utils';
/**
* @experimental
*/
export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitter<KeyProviderCallbacks>) {
private keyInfoMap: Map<string, KeyInfo>;
private readonly options: KeyProviderOptions;
private latestManuallySetKeyIndex = 0;
constructor(options: Partial<KeyProviderOptions> = {}) {
super();
this.keyInfoMap = new Map();
this.options = { ...KEY_PROVIDER_DEFAULTS, ...options };
this.on(KeyProviderEvent.KeyRatcheted, this.onKeyRatcheted);
}
/**
* callback to invoke once a key has been set for a participant
* @param key
* @param participantIdentity
* @param keyIndex
*/
protected onSetEncryptionKey(key: CryptoKey, participantIdentity?: string, keyIndex?: number) {
const keyInfo: KeyInfo = { key, participantIdentity, keyIndex };
if (!this.options.sharedKey && !participantIdentity) {
throw new Error(
'participant identity needs to be passed for encryption key if sharedKey option is false',
);
}
this.keyInfoMap.set(`${participantIdentity ?? 'shared'}-${keyIndex ?? 0}`, keyInfo);
if (keyIndex !== undefined) {
this.latestManuallySetKeyIndex = keyIndex;
}
this.emit(KeyProviderEvent.SetKey, keyInfo, keyIndex !== undefined);
}
/**
* Callback being invoked after a key has been ratcheted.
* Can happen when:
* - A decryption failure occurs and the key is auto-ratcheted
* - A ratchet request is sent (see {@link ratchetKey()})
* @param ratchetResult Contains the ratcheted chain key (exportable to other participants) and the derived new key material.
* @param participantId
* @param keyIndex
*/
protected onKeyRatcheted = (
ratchetResult: RatchetResult,
participantId?: string,
keyIndex?: number,
) => {
log.debug('key ratcheted event received', { ratchetResult, participantId, keyIndex });
};
getKeys() {
return Array.from(this.keyInfoMap.values());
}
getLatestManuallySetKeyIndex() {
return this.latestManuallySetKeyIndex;
}
getOptions() {
return this.options;
}
ratchetKey(participantIdentity?: string, keyIndex?: number) {
this.emit(KeyProviderEvent.RatchetRequest, participantIdentity, keyIndex);
}
}
/**
* A basic KeyProvider implementation intended for a single shared
* passphrase between all participants
* @experimental
*/
export class ExternalE2EEKeyProvider extends BaseKeyProvider {
ratchetInterval: number | undefined;
constructor(options: Partial<Omit<KeyProviderOptions, 'sharedKey'>> = {}) {
const opts: Partial<KeyProviderOptions> = {
...options,
sharedKey: true,
// for a shared key provider failing to decrypt for a specific participant
// should not mark the key as invalid, so we accept wrong keys forever
// and won't try to auto-ratchet
ratchetWindowSize: 0,
failureTolerance: -1,
};
super(opts);
}
/**
* Accepts a passphrase that's used to create the crypto keys.
* When passing in a string, PBKDF2 is used. (recommended for maximum compatibility across SDKs)
* When passing in an ArrayBuffer of cryptographically random numbers, HKDF is used.
*
* Note: Not all client SDKS support HKDF.
* @param key
*/
async setKey(key: string | ArrayBuffer) {
const derivedKey =
typeof key === 'string'
? await createKeyMaterialFromString(key)
: await createKeyMaterialFromBuffer(key);
this.onSetEncryptionKey(derivedKey);
}
}