feat: implement DAVE end-to-end encryption (#10921)

* feat(voice): implement DAVE E2EE encryption

* chore(voice): update dependencies

* chore(voice): update debug logs and dependency report

* feat(voice): emit and propogate DAVESession errors

* chore(voice): export dave session things

* chore(voice): move expiry numbers to consts

* feat(voice): keep track of and pass connected client IDs

* fix(voice): dont set initial transitions as pending

* feat(voice): dave encryption

* chore(voice): directly reference package name in import

* feat(voice): dave decryption

* chore(deps): update @snazzah/davey

* fix(voice): handle decryption failure tolerance

* fix(voice): move and update decryption failure logic to DAVESession

* feat(voice): propogate voice privacy code

* fix(voice): actually send a transition ready when ready

* feat(voice): propogate transitions and verification code function

* feat(voice): add dave options

* chore: resolve format change requests

* chore: emit debug messages on bad transitions

* chore: downgrade commit/welcome errors as debug messages

* chore: resolve formatting change requests

* chore: update davey dependency

* chore: add types for underlying dave session

* fix: fix rebase

* chore: change "ID" to "id" in typedocs

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
Snazzah
2025-07-13 13:02:56 -04:00
committed by GitHub
parent 3cff4d7412
commit 8bdea6232b
14 changed files with 976 additions and 143 deletions

View File

@@ -182,6 +182,12 @@ export interface VoiceConnection extends EventEmitter {
* @eventProperty
*/
on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this;
/**
* Emitted when the end-to-end encrypted session has transitioned
*
* @eventProperty
*/
on(event: 'transitioned', listener: (transitionId: number) => void): this;
/**
* Emitted when the state of the voice connection changes to a specific status
*
@@ -235,6 +241,11 @@ export class VoiceConnection extends EventEmitter {
*/
private readonly debug: ((message: string) => void) | null;
/**
* The options used to create this voice connection.
*/
private readonly options: CreateVoiceConnectionOptions;
/**
* Creates a new voice connection.
*
@@ -253,6 +264,7 @@ export class VoiceConnection extends EventEmitter {
this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);
this.onNetworkingError = this.onNetworkingError.bind(this);
this.onNetworkingDebug = this.onNetworkingDebug.bind(this);
this.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this);
const adapter = options.adapterCreator({
onVoiceServerUpdate: (data) => this.addServerPacket(data),
@@ -268,6 +280,7 @@ export class VoiceConnection extends EventEmitter {
};
this.joinConfig = joinConfig;
this.options = options;
}
/**
@@ -295,6 +308,7 @@ export class VoiceConnection extends EventEmitter {
oldNetworking.off('error', this.onNetworkingError);
oldNetworking.off('close', this.onNetworkingClose);
oldNetworking.off('stateChange', this.onNetworkingStateChange);
oldNetworking.off('transitioned', this.onNetworkingTransitioned);
oldNetworking.destroy();
}
@@ -412,14 +426,20 @@ export class VoiceConnection extends EventEmitter {
token: server.token,
sessionId: state.session_id,
userId: state.user_id,
channelId: state.channel_id!,
},
{
debug: Boolean(this.debug),
daveEncryption: this.options.daveEncryption ?? true,
decryptionFailureTolerance: this.options.decryptionFailureTolerance,
},
Boolean(this.debug),
);
networking.once('close', this.onNetworkingClose);
networking.on('stateChange', this.onNetworkingStateChange);
networking.on('error', this.onNetworkingError);
networking.on('debug', this.onNetworkingDebug);
networking.on('transitioned', this.onNetworkingTransitioned);
this.state = {
...this.state,
@@ -509,6 +529,15 @@ export class VoiceConnection extends EventEmitter {
this.debug?.(`[NW] ${message}`);
}
/**
* Propagates transitions from the underlying network instance.
*
* @param transitionId - The transition id
*/
private onNetworkingTransitioned(transitionId: number) {
this.emit('transitioned', transitionId);
}
/**
* Prepares an audio packet for dispatch.
*
@@ -694,6 +723,41 @@ export class VoiceConnection extends EventEmitter {
};
}
/**
* The current voice privacy code of the encrypted session.
*
* @remarks
* For this data to be available, the VoiceConnection must be in the Ready state,
* and the connection would have to be end-to-end encrypted.
*/
public get voicePrivacyCode() {
if (
this.state.status === VoiceConnectionStatus.Ready &&
this.state.networking.state.code === NetworkingStatusCode.Ready
) {
return this.state.networking.state.dave?.voicePrivacyCode ?? undefined;
}
return undefined;
}
/**
* Gets the verification code for a user in the session.
*
* @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session.
*/
public async getVerificationCode(userId: string): Promise<string> {
if (
this.state.status === VoiceConnectionStatus.Ready &&
this.state.networking.state.code === NetworkingStatusCode.Ready &&
this.state.networking.state.dave
) {
return this.state.networking.state.dave.getVerificationCode(userId);
}
throw new Error('Session not available');
}
/**
* Called when a subscription of this voice connection to an audio player is removed.
*