Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Jul 13, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
400a4be
feat(voice): implement DAVE E2EE encryption
Snazzah Jun 4, 2025
ec6b8b4
chore(voice): update dependencies
Snazzah Jun 4, 2025
0c716b5
chore(voice): update debug logs and dependency report
Snazzah Jun 4, 2025
4952ef6
feat(voice): emit and propogate DAVESession errors
Snazzah Jun 4, 2025
9571449
chore(voice): export dave session things
Snazzah Jun 4, 2025
6c2d9be
chore(voice): move expiry numbers to consts
Snazzah Jun 4, 2025
5461e94
feat(voice): keep track of and pass connected client IDs
Snazzah Jun 4, 2025
d44b76a
fix(voice): dont set initial transitions as pending
Snazzah Jun 4, 2025
99e10b4
feat(voice): dave encryption
Snazzah Jun 4, 2025
c196909
chore(voice): directly reference package name in import
Snazzah Jun 4, 2025
c925557
feat(voice): dave decryption
Snazzah Jun 4, 2025
bd29b62
chore(deps): update @snazzah/davey
Snazzah Jun 5, 2025
04dac3a
fix(voice): handle decryption failure tolerance
Snazzah Jun 5, 2025
5feb52c
fix(voice): move and update decryption failure logic to DAVESession
Snazzah Jun 6, 2025
890c20d
feat(voice): propogate voice privacy code
Snazzah Jun 8, 2025
f8fa827
fix(voice): actually send a transition ready when ready
Snazzah Jun 8, 2025
333d50e
feat(voice): propogate transitions and verification code function
Snazzah Jun 8, 2025
fb7c39a
feat(voice): add dave options
Snazzah Jun 8, 2025
4102bac
Merge branch 'main' into feat/dave-protocol
Snazzah Jun 8, 2025
d6eb94e
chore: resolve format change requests
Snazzah Jun 9, 2025
22f1f06
chore: emit debug messages on bad transitions
Snazzah Jun 9, 2025
f054fc6
chore: downgrade commit/welcome errors as debug messages
Snazzah Jun 9, 2025
b74395c
chore: resolve formatting change requests
Snazzah Jun 9, 2025
c1f3cc6
chore: update davey dependency
Snazzah Jun 10, 2025
00b8717
chore: add types for underlying dave session
Snazzah Jun 10, 2025
e58eb4f
Merge branch 'main' into feat/dave-protocol
Snazzah Jun 15, 2025
59ff18d
Merge branch 'main' into feat/dave-protocol
Snazzah Jul 3, 2025
9520210
fix: fix rebase
Snazzah Jul 3, 2025
50ad3cb
chore: change "ID" to "id" in typedocs
Snazzah Jul 3, 2025
0388570
Merge branch 'main' into feat/dave-protocol
Jiralite Jul 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/voice/__tests__/VoiceConnection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ describe('VoiceConnection#configureNetworking', () => {
sessionId: state.session_id,
userId: state.user_id,
},
false,
{
daveEncryption: true,
debug: false,
decryptionFailureTolerance: undefined,
},
);
expect(voiceConnection.state).toMatchObject({
status: VoiceConnectionStatus.Connecting,
Expand Down
32 changes: 1 addition & 31 deletions packages/voice/__tests__/VoiceReceiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { Buffer } from 'node:buffer';
import { once } from 'node:events';
import process from 'node:process';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import { VoiceOpcodes } from 'discord-api-types/voice/v8';
import { describe, test, expect, vitest, beforeEach } from 'vitest';
import {
RTP_PACKET_DESKTOP,
Expand Down Expand Up @@ -141,36 +141,6 @@ describe('VoiceReceiver', () => {
userId: '123abc',
});
});

test('CLIENT_CONNECT packet', () => {
const spy = vitest.spyOn(receiver.ssrcMap, 'update');
receiver['onWsPacket']({
op: VoiceOpcodes.ClientConnect,
d: {
audio_ssrc: 123,
video_ssrc: 43,
user_id: '123abc',
},
});
expect(spy).toHaveBeenCalledWith({
audioSSRC: 123,
videoSSRC: 43,
userId: '123abc',
});
receiver['onWsPacket']({
op: VoiceOpcodes.ClientConnect,
d: {
audio_ssrc: 123,
video_ssrc: 0,
user_id: '123abc',
},
});
expect(spy).toHaveBeenCalledWith({
audioSSRC: 123,
videoSSRC: undefined,
userId: '123abc',
});
});
});

describe('decrypt', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/voice/__tests__/VoiceWebSocket.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type EventEmitter, once } from 'node:events';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import { VoiceOpcodes } from 'discord-api-types/voice/v8';
import { describe, test, expect, beforeEach } from 'vitest';
import WS from 'vitest-websocket-mock';
import { VoiceWebSocket } from '../src/networking/VoiceWebSocket';
Expand Down
3 changes: 2 additions & 1 deletion packages/voice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@types/ws": "^8.18.1",
"discord-api-types": "^0.38.1",
"discord-api-types": "^0.38.11",
"prism-media": "^1.3.5",
"tslib": "^2.8.1",
"ws": "^8.18.1"
Expand All @@ -75,6 +75,7 @@
"@discordjs/scripts": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@noble/ciphers": "^1.2.1",
"@snazzah/davey": "^0.1.5",
"@types/node": "^22.15.2",
"@vitest/coverage-v8": "^3.1.1",
"cross-env": "^7.0.3",
Expand Down
66 changes: 65 additions & 1 deletion packages/voice/src/VoiceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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),
Expand All @@ -268,6 +280,7 @@ export class VoiceConnection extends EventEmitter {
};

this.joinConfig = joinConfig;
this.options = options;
}

/**
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions packages/voice/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
VoiceUDPSocket,
VoiceWebSocket,
type SocketConfig,
DAVESession,
} from './networking/index.js';

export {
Expand Down
13 changes: 13 additions & 0 deletions packages/voice/src/joinVoiceChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@ import type { DiscordGatewayAdapterCreator } from './util/adapter';
export interface CreateVoiceConnectionOptions {
adapterCreator: DiscordGatewayAdapterCreator;

/**
* Whether to use the DAVE protocol for end-to-end encryption. Defaults to true.
*/
daveEncryption?: boolean | undefined;

/**
* If true, debug messages will be enabled for the voice connection and its
* related components. Defaults to false.
*/
debug?: boolean | undefined;

/**
* The amount of consecutive decryption failures needed to try to
* re-initialize the end-to-end encrypted session to recover. Defaults to 24.
*/
decryptionFailureTolerance?: number | undefined;
}

/**
Expand Down Expand Up @@ -61,5 +72,7 @@ export function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoi
return createVoiceConnection(joinConfig, {
adapterCreator: options.adapterCreator,
debug: options.debug,
daveEncryption: options.daveEncryption,
decryptionFailureTolerance: options.decryptionFailureTolerance,
});
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy