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

@@ -2,9 +2,10 @@
import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import type { VoiceConnection } from '../VoiceConnection';
import type { ConnectionData } from '../networking/Networking';
import type { VoiceReceivePayload } from 'discord-api-types/voice/v8';
import { VoiceOpcodes } from 'discord-api-types/voice/v8';
import { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';
import { NetworkingStatusCode, type ConnectionData } from '../networking/Networking';
import { methods } from '../util/Secretbox';
import {
AudioReceiveStream,
@@ -69,25 +70,11 @@ export class VoiceReceiver {
* @param packet - The received packet
* @internal
*/
public onWsPacket(packet: any) {
if (packet.op === VoiceOpcodes.ClientDisconnect && typeof packet.d?.user_id === 'string') {
public onWsPacket(packet: VoiceReceivePayload) {
if (packet.op === VoiceOpcodes.ClientDisconnect) {
this.ssrcMap.delete(packet.d.user_id);
} else if (
packet.op === VoiceOpcodes.Speaking &&
typeof packet.d?.user_id === 'string' &&
typeof packet.d?.ssrc === 'number'
) {
} else if (packet.op === VoiceOpcodes.Speaking) {
this.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });
} else if (
packet.op === VoiceOpcodes.ClientConnect &&
typeof packet.d?.user_id === 'string' &&
typeof packet.d?.audio_ssrc === 'number'
) {
this.ssrcMap.update({
userId: packet.d.user_id,
audioSSRC: packet.d.audio_ssrc,
videoSSRC: packet.d.video_ssrc === 0 ? undefined : packet.d.video_ssrc,
});
}
}
@@ -143,11 +130,12 @@ export class VoiceReceiver {
* @param mode - The encryption mode
* @param nonce - The nonce buffer used by the connection for encryption
* @param secretKey - The secret key used by the connection for encryption
* @param userId - The user id that sent the packet
* @returns The parsed Opus packet
*/
private parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array) {
private parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array, userId: string) {
let packet = this.decrypt(buffer, mode, nonce, secretKey);
if (!packet) return;
if (!packet) throw new Error('Failed to parse packet');
// Strip decrypted RTP Header Extension if present
// The header is only indicated in the original data, so compare with buffer first
@@ -156,6 +144,16 @@ export class VoiceReceiver {
packet = packet.subarray(4 * headerExtensionLength);
}
// Decrypt packet if in a DAVE session.
if (
this.voiceConnection.state.status === VoiceConnectionStatus.Ready &&
(this.voiceConnection.state.networking.state.code === NetworkingStatusCode.Ready ||
this.voiceConnection.state.networking.state.code === NetworkingStatusCode.Resuming)
) {
const daveSession = this.voiceConnection.state.networking.state.dave;
if (daveSession) packet = daveSession.decrypt(packet, userId)!;
}
return packet;
}
@@ -178,16 +176,17 @@ export class VoiceReceiver {
if (!stream) return;
if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {
const packet = this.parsePacket(
msg,
this.connectionData.encryptionMode,
this.connectionData.nonceBuffer,
this.connectionData.secretKey,
);
if (packet) {
stream.push(packet);
} else {
stream.destroy(new Error('Failed to parse packet'));
try {
const packet = this.parsePacket(
msg,
this.connectionData.encryptionMode,
this.connectionData.nonceBuffer,
this.connectionData.secretKey,
userData.userId,
);
if (packet) stream.push(packet);
} catch (error) {
stream.destroy(error as Error);
}
}
}