fix(dave): decrypt incoming DAVE-encrypted audio and handle passthrough frames#3202
Closed
Phydra11 wants to merge 7 commits intoPycord-Development:masterfrom
Closed
fix(dave): decrypt incoming DAVE-encrypted audio and handle passthrough frames#3202Phydra11 wants to merge 7 commits intoPycord-Development:masterfrom
Phydra11 wants to merge 7 commits intoPycord-Development:masterfrom
Conversation
When dave.ready is False or the SSRC is not yet in ssrc_user_map, decrypt_rtp returned None causing the packet to be silently dropped in callback. Fall back to the nacl-decrypted raw_payload instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Call sink.init(client) in AudioReader.__init__ — was commented out, causing sink.vc = None and crash in opus.py assert - On DAVE decrypt failure, leave decrypted_data as None instead of OPUS_SILENCE so the raw_payload fallback is used (handles UnencryptedWhenPassthroughDisabled during MLS transition) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes to make DAVE receive work end-to-end: 1. Extension-stripping (reader.py): For extended RTP packets the outer XChaCha decrypt returns [8B RTP extension values][DAVE frame]. davey.decrypt() must receive only the DAVE frame, so use raw_payload (= result[8:]) as dave_input for extended packets. Passing the full result caused AES-128-GCM auth-tag mismatch → NoValidCryptorFound. 2. Self-commit (gateway.py): After process_proposals() returns a CommitWelcome, immediately call process_commit(result.commit) to establish epoch-1 keys from our own commit. This prevents a mismatch when Discord later sends mls_welcome (op30) for a different group context instead of echoing our commit via mls_commit_transition (op29). 3. Skip op29/op30 when already ready (gateway.py): If the session is already ready from the self-commit, skip process_commit/process_welcome for incoming op29/op30 to avoid double-advancing or overwriting with mismatched key material. Still send transition_ready if needed. Result: DAVE decrypt FIRST SUCCESS confirmed (raw_len=138, out_len=126). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes in PacketDecryptor.decrypt_rtp:
1. Do not call update_extended_header() on DAVE-decrypted audio.
davey.decrypt() returns pure Opus bytes. The extension header was
already parsed during outer XChaCha decryption. Calling
update_extended_header() on the Opus output misinterprets the Opus
TOC byte as extension length, strips bytes from the frame, and
causes OpusError("corrupted stream") in the decoder thread.
2. When DAVE decrypt raises an exception, fall back to Opus silence
(b'\xf8\xff\xfe') instead of leaving decrypted_data as None.
The previous None-based fallback passed DAVE-ciphertext garbage to
the Opus decoder (in PCM-mode sinks), killing the router thread.
3. When DAVE is active but the session is not yet ready (MLS handshake
in progress), also use Opus silence as fallback. Passing cipher-
text to the Opus decoder during the handshake window caused the
same crash.
Confirmed working: WavSink test produces correct, audible audio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Discord sporadically sends passthrough frames (~5% of all frames) even
when DAVE is active. These frames are rejected by davey with
UnencryptedWhenPassthroughDisabled because passthrough mode is disabled.
Root cause (reverse-engineered from wire data):
Passthrough frames have the format:
[raw_opus][dave_supp_block(supp_size B)][rtp_padding]
The DAVE supplemental block ends with supp_size(1B) + 0xFAFA(2B);
supp_size counts the entire block including itself and the magic bytes.
RTP padding (RFC 3550): last byte = N, strip N bytes from end.
Fix in decrypt_rtp:
- Strip RTP padding (if packet.padding), then strip the DAVE block
- Use the recovered raw Opus directly instead of silence
- Placeholder frames (all-0xFF, uniform bytes, no 0xFAFA) → silence
Additional improvements:
- Dynamic extension offset: use update_extended_header() return value
instead of hardcoded result[8:] in _decrypt_rtp_aead_xchacha20_*
- Richer diagnostic logging: consec failures, time since last success,
RTP sequence number, ext_hdr bytes, outer_head, dave_tail
Result: audio is clear without dropouts, router thread never crashes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove verbose per-frame hex dumps (dave_tail, outer_head, ext_hdr, raw_payload_head/tail, since_last_ok) that were added for debugging the passthrough frame format. Keep only the essential log lines: - DEBUG when DAVE decryption first becomes active for an SSRC - INFO when decryption recovers after a burst of failures - WARNING on the first failure in a burst or a new key generation Also remove the unused _dave_failure and _dave_last_success_time counters, replace bare b'\xf8\xff\xfe' literals with OPUS_SILENCE, and drop the per-request debug log from _decrypt_rtp_aead_xchacha20_*. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for opening this pull request! This pull request can be checked-out with: git fetch origin pull/3202/head:pr-3202
git checkout pr-3202This pull request can be installed with: pip install git+https://github.com/Pycord-Development/pycord@refs/pull/3202/head |
Member
|
You don't understand what you are working with, this is clearly 100% slop. Furthermore this has already been worked on in #3159. If you have issues with id comment there directly. |
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
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Discord added mandatory DAVE (Discord Audio Video Encryption) E2EE for voice channels in March 2026. Without this fix, bots using py-cord cannot receive or decode any voice audio because all incoming frames are DAVE-encrypted.
This PR fixes the voice receive pipeline so that incoming DAVE-encrypted audio frames are correctly decrypted using the
daveylibrary (Python bindings for Discord's MLS-based encryption).Changes in
discord/voice/receive/reader.py—PacketDecryptor:decrypt_rtp()usingdavey.DaveSession.decrypt()_decrypt_rtp_aead_xchacha20_poly1305_rtpsize()to use the dynamic offset returned byupdate_extended_header()instead of a hardcodedresult[8:]\xf8\xff\xfe) for decrypt errors instead of passing cipher bytes to the Opus decoder, which would crash the router threadpacket.decrypted_datais set directly —update_extended_header()is NOT called on the decrypted output (the RTP extension was already handled in the XChaCha layer)Passthrough frame format (reverse-engineered from live Discord traffic):
[raw_opus][dave_supp_block (supp_size bytes)][rtp_padding]supp_size (1 byte) + 0xFAFA (2 bytes); supp_size counts the whole block (typically 12 bytes)Tested: Real Discord voice channel with DAVE E2EE active, 883 packets over 17.7 seconds, zero crashes, clean audio output verified via WavSink.
AI was used to assist with development of this PR. I understand fully what the code does.
Information
Checklist
type: ignorecomments were used, a comment is also left explaining why.