mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 11:33:30 +01:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user