-
-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Open
Snazzah
wants to merge
26
commits into
discordjs:main
Choose a base branch
from
Snazzah:feat/dave-protocol
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+981
−163
Open
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
400a4be
feat(voice): implement DAVE E2EE encryption
Snazzah ec6b8b4
chore(voice): update dependencies
Snazzah 0c716b5
chore(voice): update debug logs and dependency report
Snazzah 4952ef6
feat(voice): emit and propogate DAVESession errors
Snazzah 9571449
chore(voice): export dave session things
Snazzah 6c2d9be
chore(voice): move expiry numbers to consts
Snazzah 5461e94
feat(voice): keep track of and pass connected client IDs
Snazzah d44b76a
fix(voice): dont set initial transitions as pending
Snazzah 99e10b4
feat(voice): dave encryption
Snazzah c196909
chore(voice): directly reference package name in import
Snazzah c925557
feat(voice): dave decryption
Snazzah bd29b62
chore(deps): update @snazzah/davey
Snazzah 04dac3a
fix(voice): handle decryption failure tolerance
Snazzah 5feb52c
fix(voice): move and update decryption failure logic to DAVESession
Snazzah 890c20d
feat(voice): propogate voice privacy code
Snazzah f8fa827
fix(voice): actually send a transition ready when ready
Snazzah 333d50e
feat(voice): propogate transitions and verification code function
Snazzah fb7c39a
feat(voice): add dave options
Snazzah 4102bac
Merge branch 'main' into feat/dave-protocol
Snazzah d6eb94e
chore: resolve format change requests
Snazzah 22f1f06
chore: emit debug messages on bad transitions
Snazzah f054fc6
chore: downgrade commit/welcome errors as debug messages
Snazzah b74395c
chore: resolve formatting change requests
Snazzah c1f3cc6
chore: update davey dependency
Snazzah 00b8717
chore: add types for underlying dave session
Snazzah e58eb4f
Merge branch 'main' into feat/dave-protocol
Snazzah File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
import { Buffer } from 'node:buffer'; | ||
import { EventEmitter } from 'node:events'; | ||
import type { VoiceDavePrepareEpochData, VoiceDavePrepareTransitionData } from 'discord-api-types/voice/v8'; | ||
|
||
const LIBRARY_NAME = '@snazzah/davey'; | ||
let Davey: any = null; | ||
|
||
// eslint-disable-next-line no-async-promise-executor | ||
export const daveLoadPromise = new Promise<void>(async (resolve) => { | ||
try { | ||
const lib = await import(LIBRARY_NAME); | ||
Davey = lib; | ||
} catch {} | ||
|
||
resolve(); | ||
}); | ||
|
||
export interface TransitionResult { | ||
success: boolean; | ||
transitionId: number; | ||
} | ||
|
||
/** | ||
* The maximum DAVE protocol version supported. | ||
*/ | ||
export function getMaxProtocolVersion(): number | null { | ||
return Davey?.DAVE_PROTOCOL_VERSION; | ||
} | ||
|
||
export interface DAVESession extends EventEmitter { | ||
Snazzah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
on(event: 'error', listener: (error: Error) => void): this; | ||
on(event: 'debug', listener: (message: string) => void): this; | ||
on(event: 'keyPackage', listener: (message: Buffer) => void): this; | ||
} | ||
|
||
/** | ||
* Manages the DAVE protocol group session. | ||
*/ | ||
export class DAVESession extends EventEmitter { | ||
/** | ||
* The channel ID represented by this session. | ||
*/ | ||
public channelId: string; | ||
|
||
/** | ||
* The user ID represented by this session. | ||
*/ | ||
public userId: string; | ||
|
||
/** | ||
* The protocol version being used. | ||
*/ | ||
public protocolVersion: number; | ||
|
||
/** | ||
* The pending transition. | ||
*/ | ||
private pendingTransition?: VoiceDavePrepareTransitionData | undefined; | ||
|
||
/** | ||
* Whether this session was downgraded previously. | ||
*/ | ||
private downgraded = false; | ||
|
||
/** | ||
* The underlying DAVE Session of this wrapper. | ||
*/ | ||
public session: any; | ||
Snazzah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public constructor(protocolVersion: number, userId: string, channelId: string) { | ||
if (Davey === null) | ||
throw new Error( | ||
`Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed. | ||
- Use the generateDependencyReport() function for more information.\n`, | ||
); | ||
|
||
super(); | ||
|
||
this.protocolVersion = protocolVersion; | ||
this.userId = userId; | ||
this.channelId = channelId; | ||
} | ||
|
||
/** | ||
* Re-initializes (or initializes) the underlying session. | ||
*/ | ||
public reinit() { | ||
if (this.protocolVersion > 0) { | ||
if (this.session) { | ||
this.session.reinit(this.protocolVersion, this.userId, this.channelId); | ||
this.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`); | ||
} else { | ||
this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId); | ||
this.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`); | ||
} | ||
|
||
this.emit('keyPackage', this.session.getSerializedKeyPackage()); | ||
} else if (this.session) { | ||
this.session.reset(); | ||
this.session.setPassthroughMode(true, 10); | ||
Snazzah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.emit('debug', 'Session reset'); | ||
} | ||
} | ||
|
||
/** | ||
* Set the external sender for this session. | ||
* | ||
* @param externalSender - The external sender | ||
*/ | ||
public setExternalSender(externalSender: Buffer) { | ||
this.session.setExternalSender(externalSender); | ||
this.emit('debug', 'Set MLS external sender'); | ||
} | ||
|
||
/** | ||
* Prepare for a transition. | ||
* | ||
* @param data - The transition data | ||
* @returns Whether we should signal to the voice server that we are ready | ||
*/ | ||
public prepareTransition(data: VoiceDavePrepareTransitionData) { | ||
this.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`); | ||
this.pendingTransition = data; | ||
|
||
// When the included transition ID is 0, the transition is for (re)initialization and it can be executed immediately. | ||
if (data.transition_id === 0) { | ||
this.executeTransition(data.transition_id); | ||
} else { | ||
if (data.protocol_version === 0) this.session?.setPassthroughMode(true, 120); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Execute a transition. | ||
* | ||
* @param transitionId - The transition id to execute on | ||
*/ | ||
public executeTransition(transitionId: number) { | ||
this.emit('debug', `Executing transition (${transitionId})`); | ||
if (!this.pendingTransition) return; | ||
Snazzah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let transitioned = false; | ||
if (transitionId === this.pendingTransition.transition_id) { | ||
const oldVersion = this.protocolVersion; | ||
this.protocolVersion = this.pendingTransition.protocol_version; | ||
|
||
// Handle upgrades & defer downgrades | ||
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) { | ||
this.downgraded = true; | ||
this.emit('debug', 'Session downgraded'); | ||
} else if (transitionId > 0 && this.downgraded) { | ||
this.downgraded = false; | ||
this.session?.setPassthroughMode(true, 10); | ||
this.emit('debug', 'Session upgraded'); | ||
} | ||
|
||
// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time | ||
transitioned = true; | ||
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`); | ||
} | ||
|
||
this.pendingTransition = undefined; | ||
return transitioned; | ||
} | ||
|
||
/** | ||
* Prepare for a new epoch. | ||
* | ||
* @param data - The epoch data | ||
*/ | ||
public prepareEpoch(data: VoiceDavePrepareEpochData) { | ||
this.emit('debug', `Preparing for epoch (${data.epoch})`); | ||
if (data.epoch === 1) { | ||
this.protocolVersion = data.protocol_version; | ||
this.reinit(); | ||
} | ||
} | ||
|
||
/** | ||
* Processes proposals from the MLS group. | ||
* | ||
* @param payload - The proposals or proposal refs buffer | ||
* @returns The payload to send back to the voice server, if there is one | ||
*/ | ||
public processProposals(payload: Buffer): Buffer | undefined { | ||
const optype = payload.readUInt8(0); | ||
// TODO store clients connected and pass in here | ||
const { commit, welcome } = this.session.processProposals(optype, payload.subarray(1)); | ||
this.emit('debug', 'MLS proposals processed'); | ||
if (!commit) return; | ||
return welcome ? Buffer.concat([commit, welcome]) : commit; | ||
} | ||
|
||
/** | ||
* Processes a commit from the MLS group. | ||
* | ||
* @param payload - The payload | ||
* @returns The transaction ID and whether it was successful | ||
*/ | ||
public processCommit(payload: Buffer): TransitionResult { | ||
const transitionId = payload.readUInt16BE(0); | ||
try { | ||
this.session.processCommit(payload.subarray(2)); | ||
this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion }; | ||
this.emit('debug', `MLS commit processed (transition id: ${transitionId})`); | ||
return { transitionId, success: true }; | ||
} catch { | ||
// TODO | ||
// this.emit("warn", `MLS commit errored: ${e}`); | ||
return { transitionId, success: false }; | ||
} | ||
} | ||
|
||
/** | ||
* Processes a welcome from the MLS group. | ||
* | ||
* @param payload - The payload | ||
* @returns The transaction ID and whether it was successful | ||
*/ | ||
public processWelcome(payload: Buffer): TransitionResult { | ||
const transitionId = payload.readUInt16BE(0); | ||
try { | ||
this.session.processWelcome(payload.subarray(2)); | ||
this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion }; | ||
this.emit('debug', `MLS welcome processed (transition id: ${transitionId})`); | ||
return { transitionId, success: true }; | ||
} catch { | ||
// TODO | ||
// this.emit("warn", `MLS welcome errored: ${e}`); | ||
return { transitionId, success: false }; | ||
} | ||
} | ||
|
||
/** | ||
* Resets the session. | ||
*/ | ||
public destroy() { | ||
try { | ||
this.session.reset(); | ||
Snazzah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} catch {} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.