{"id":49581671,"url":"https://github.com/official-alex/libundave","last_synced_at":"2026-05-03T20:40:02.185Z","repository":{"id":336107757,"uuid":"1148299225","full_name":"official-alex/libundave","owner":"official-alex","description":"reversing \u0026 decrypting \"DAVE\", discords ETEE protocol :D","archived":false,"fork":false,"pushed_at":"2026-02-02T20:22:53.000Z","size":54,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-03T10:35:40.665Z","etag":null,"topics":["dave","decrypt","discord","encryption","reverse-engineering"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/official-alex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-02T20:03:32.000Z","updated_at":"2026-02-02T21:29:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/official-alex/libundave","commit_stats":null,"previous_names":["official-alex/libundave"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/official-alex/libundave","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/official-alex%2Flibundave","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/official-alex%2Flibundave/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/official-alex%2Flibundave/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/official-alex%2Flibundave/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/official-alex","download_url":"https://codeload.github.com/official-alex/libundave/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/official-alex%2Flibundave/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32584646,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dave","decrypt","discord","encryption","reverse-engineering"],"created_at":"2026-05-03T20:40:00.378Z","updated_at":"2026-05-03T20:40:02.166Z","avatar_url":"https://github.com/official-alex.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# My Research on Discord's \"DAVE\" Protocol\n\n---\n\n## TL;DR - Can This Actually Be Done?\n\nAfter spending way too much time digging through Discord's open-source code and documentation, here's what I found:\n\n**Yes, it's absolutely possible to decrypt DAVE-encrypted voice traffic.** Here's why:\n\n1. When you join a call with valid credentials, you're a legit participant in the MLS group\n2. Discord literally sends you the Welcome message with all the group secrets\n3. From there, you can derive every sender's decryption key\n4. The actual decryption is just standard AES-128-GCM\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is MLS?\u003c/summary\u003e\n\n**MLS (Messaging Layer Security)** is a protocol for end-to-end encrypted group communication, defined in RFC 9420. Think of it like Signal's protocol but designed specifically for groups. It lets everyone in a group agree on shared encryption keys without any server (including Discord) being able to see those keys.\n\nThe clever part is how it handles people joining and leaving - it uses a tree structure so you don't have to redo everything when someone joins.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is AES-128-GCM?\u003c/summary\u003e\n\n**AES-128-GCM** is an encryption algorithm. Breaking it down:\n- **AES** = Advanced Encryption Standard (the actual cipher)\n- **128** = key size in bits (16 bytes)\n- **GCM** = Galois/Counter Mode (a mode that provides both encryption AND authentication)\n\nThe \"authentication\" part is important - it means you can detect if someone tampered with the data. That's what the \"auth tag\" is for.\n\u003c/details\u003e\n\nThe catch? Nobody's publicly documented how to do this yet. I'd be the first to fully reverse engineer and implement it.\n\n---\n\n## What's In This Doc\n\n1. [How The Whole Thing Works](#1-how-the-whole-thing-works)\n2. [The MLS Key Exchange (The Hard Part)](#2-the-mls-key-exchange-the-hard-part)\n3. [Voice Gateway Stuff](#3-voice-gateway-stuff)\n4. [How Frames Get Encrypted/Decrypted](#4-how-frames-get-encrypteddecrypted)\n5. [The Network Layer](#5-the-network-layer)\n6. [System Architecture](#6-system-architecture)\n7. [What I Found In libdave's Source Code](#7-what-i-found-in-libdaves-source-code)\n8. [Libraries I'll Need](#8-libraries-ill-need)\n9. [Code Examples](#9-code-examples)\n\n---\n\n## 1. How The Whole Thing Works\n\nSo Discord's DAVE system has two encryption layers. Here's a diagram I made:\n\n```\n╔══════════════════════════════════════════════════════════════╗\n║                    DISCORD DAVE ARCHITECTURE                 ║\n╠══════════════════════════════════════════════════════════════╣\n║                                                              ║\n║    ┌──────────┐       WebSocket        ┌──────────────┐      ║\n║    │  Client  │◄──────────────────────►│Voice Gateway │      ║\n║    │          │    MLS + Signaling     │   (wss://)   │      ║\n║    └────┬─────┘                        └──────────────┘      ║\n║         │                                                    ║\n║         │         UDP/RTP (Encrypted Frames)                 ║\n║         ▼                                                    ║\n║    ┌──────────┐                        ┌──────────────┐      ║\n║    │   SFU    │◄──────────────────────►│Other Clients │      ║\n║    │ (Relay)  │                        │              │      ║\n║    └──────────┘                        └──────────────┘      ║\n║                                                              ║\n║    TWO LAYERS OF ENCRYPTION:                                 ║\n║    Layer 1: Transport (DTLS/SRTP) - between you and Discord  ║\n║    Layer 2: E2EE (DAVE/MLS) - between you and other users    ║\n║             ^^^ The SFU can't see inside this one            ║\n║             (Discord literally cannot decrypt your calls)    ║\n║                                                              ║\n╚══════════════════════════════════════════════════════════════╝\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is an SFU?\u003c/summary\u003e\n\n**SFU (Selective Forwarding Unit)** is basically a smart relay server. Instead of everyone sending their audio to everyone else (which would be a nightmare with 10+ people), everyone sends to the SFU, and the SFU forwards it to everyone else.\n\nThe \"selective\" part means it can choose what to forward - like not sending video if someone's connection is bad, or prioritizing the person currently speaking.\n\nWith DAVE, the SFU can still do its job because it only needs the RTP headers (which aren't E2E encrypted), but it can't actually hear what anyone is saying.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is SRTP/DTLS?\u003c/summary\u003e\n\n**SRTP (Secure Real-time Transport Protocol)** is encryption for media streams. It wraps RTP packets so they're encrypted between you and the server.\n\n**DTLS (Datagram Transport Layer Security)** is basically TLS but for UDP. It's used to set up the SRTP keys.\n\nThese provide the \"transport layer\" encryption - protecting data between you and Discord's servers. DAVE adds another layer on top that Discord can't decrypt.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is E2EE?\u003c/summary\u003e\n\n**E2EE (End-to-End Encryption)** means only the people in the conversation can decrypt the messages. The servers in between (Discord, in this case) can pass the encrypted data around but can't read it.\n\nThis is different from regular encryption where the server decrypts incoming data and re-encrypts it for the recipient.\n\u003c/details\u003e\n\nThe important thing here is that Layer 2 (the E2EE part) means Discord's servers literally cannot decrypt voice data. But since I'm a participant in the call, I get the keys. Thanks Discord!\n\n### Protocol Specs (DAVE v1.1)\n\nFrom what I dug up, here are the actual parameters they use:\n\n| What                | Value                                      |\n|---------------------|-------------------------------------------|\n| MLS Version         | 1.0 (RFC 9420)                            |\n| MLS Ciphersuite     | `DHKEMP256_AES128GCM_SHA256_P256` (ID: 2) |\n| Media Cipher        | AES-128-GCM                               |\n| Key Size            | 16 bytes                                  |\n| Nonce               | 12 bytes (but only 4 bytes sent in frame) |\n| Auth Tag            | 8 bytes (truncated from the usual 16)     |\n| Magic Marker        | `0xFAFA`                                  |\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What does that ciphersuite name mean?\u003c/summary\u003e\n\n`DHKEMP256_AES128GCM_SHA256_P256` breaks down to:\n- **DHKEM** = Diffie-Hellman Key Encapsulation Mechanism (how keys are exchanged)\n- **P256** = The elliptic curve used (also called secp256r1 or prime256v1)\n- **AES128GCM** = The encryption algorithm for actual data\n- **SHA256** = The hash function used for various operations\n\nIt's just a fancy way of saying \"use these specific crypto algorithms together.\"\n\u003c/details\u003e\n\n---\n\n## 2. The MLS Key Exchange (The Hard Part)\n\nThis is where it gets complicated. MLS (Messaging Layer Security) is how everyone in the call agrees on encryption keys without Discord being able to see them. It's actually pretty cool cryptography - I'll give them that.\n\n### How The Lifecycle Works\n\n```\n╔═══════════════════════════════════════════════════════════════════════╗\n║                         MLS GROUP LIFECYCLE                           ║\n╠═══════════════════════════════════════════════════════════════════════╣\n║                                                                       ║\n║   1. INIT                  2. KEY PACKAGE             3. PROPOSALS    ║\n║   ┌─────────────┐         ┌─────────────┐          ┌─────────────┐    ║\n║   │  Generate   │         │  Send Key   │          │  Voice GW   │    ║\n║   │  Signature  │────────►│  Package    │─────────►│  Sends Add/ │    ║\n║   │  Key Pair   │         │  to Server  │          │  Remove     │    ║\n║   └─────────────┘         └─────────────┘          └──────┬──────┘    ║\n║                                                           │           ║\n║                                                           ▼           ║\n║   6. DECRYPT               5. WELCOME               4. COMMIT         ║\n║   ┌─────────────┐         ┌─────────────┐          ┌─────────────┐    ║\n║   │  Now you    │         │  New member │          │  Someone    │    ║\n║   │  can export │◄────────│  gets the   │◄─────────│  commits    │    ║\n║   │  sender keys│         │  Welcome!   │          │  proposals  │    ║\n║   └─────────────┘         └─────────────┘          └─────────────┘    ║\n║                                                                       ║\n╚═══════════════════════════════════════════════════════════════════════╝\n```\n\nBasically: you generate keys → send a \"key package\" → Discord proposes adding you → someone commits → you get a Welcome message → now you're in the group and can derive everyone's keys. Pretty straightforward once you understand the flow.\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What's a \"Key Package\"?\u003c/summary\u003e\n\nA **Key Package** is like your \"membership application\" to join an MLS group. It contains:\n- Your public encryption key (so others can encrypt stuff for you)\n- Your public signature key (so others can verify messages are from you)\n- Your identity/credential (in Discord's case, your user ID)\n- Some metadata like when it expires\n\nYou generate this before joining, send it to the server, and when someone wants to add you to the group, they use your key package to encrypt the welcome message that only you can decrypt.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What's a \"Commit\" and \"Welcome\"?\u003c/summary\u003e\n\nIn MLS, changes to the group (adding/removing members) happen through **Proposals** and **Commits**:\n\n- **Proposal**: \"Hey, I think we should add user X\" or \"User Y left\"\n- **Commit**: \"OK, I'm making these proposals official\" - this actually changes the group state\n- **Welcome**: A special message sent to new members containing everything they need to join (encrypted so only they can read it)\n\nOnly one person can commit at a time (the one who \"wins\" gets their commit accepted), which prevents weird race conditions.\n\u003c/details\u003e\n\n### The Key Package\n\nI found this in their `session.cpp` file - this is how they generate the key package:\n\n```cpp\njoinKeyPackage_ = std::make_unique\u003c::mlspp::KeyPackage\u003e(\n    ciphersuite,\n    joinInitPrivateKey_-\u003epublic_key,\n    *selfLeafNode_,\n    LeafNodeExtensionsForProtocolVersion(protocolVersion_),\n    *selfSigPrivateKey_\n);\n```\n\nWhat's actually in there:\n- Your HPKE public key (for encryption)\n- Your signature public key (for auth)\n- Your credential (just your Discord user ID as a 64-bit big-endian number)\n- Lifetime set to basically forever (`not_before=0, not_after=2^64-1`) — because who needs key rotation anyway? /s\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is HPKE?\u003c/summary\u003e\n\n**HPKE (Hybrid Public Key Encryption)** is a standard (RFC 9180) for encrypting messages using public key cryptography.\n\nThe \"hybrid\" part means it combines:\n1. Asymmetric crypto (like ECDH) to establish a shared secret\n2. Symmetric crypto (like AES) to actually encrypt the data\n\nThis is more efficient than encrypting everything with public key crypto directly, which is slow for large data.\n\u003c/details\u003e\n\n### How Sender Keys Get Derived (This Is The Good Stuff)\n\nThis is the critical part I found in their code. Each person in the call has their own encryption key, and here's how you derive it:\n\n```cpp\n// From session.cpp - GetKeyRatchet()\nstd::unique_ptr\u003cKeyRatchet\u003e Session::GetKeyRatchet(std::string const\u0026 userId) const noexcept\n{\n    // Turn the user ID string into a little-endian 64-bit number\n    auto u64userId = strtoull(userId.c_str(), nullptr, 10);\n    auto userIdBytes = ::mlspp::bytes_ns::bytes(sizeof(u64userId));\n    memcpy(userIdBytes.data(), \u0026u64userId, sizeof(u64userId));\n\n    // Export the base secret from the MLS group\n    // Label: \"Discord Secure Frames v0\"\n    // Context: the user's ID (little-endian)\n    // Length: 16 bytes\n    auto baseSecret = currentState_-\u003edo_export(\n        Session::USER_MEDIA_KEY_BASE_LABEL,  // \"Discord Secure Frames v0\"\n        userIdBytes,\n        kAesGcm128KeyBytes  // 16\n    );\n\n    return std::make_unique\u003cMlsKeyRatchet\u003e(\n        currentState_-\u003ecipher_suite(),\n        std::move(baseSecret)\n    );\n}\n```\n\nSo the formula is basically:\n\n```\nbase_secret = MLS-Exporter(\n    label = \"Discord Secure Frames v0\",\n    context = user_id_as_little_endian_64bit,\n    length = 16\n)\n\nkey_ratchet = HashRatchet(ciphersuite, base_secret)\nactual_key = key_ratchet.get(generation)\n```\n\nThe `generation` comes from the top byte of the nonce in each frame. More on that later.\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is a Key Ratchet / Hash Ratchet?\u003c/summary\u003e\n\nA **Key Ratchet** is a mechanism for generating a sequence of keys from a single \"base secret.\" It's called a ratchet because it only goes forward - you can derive key #5 from the base, but you can't go backwards from key #5 to get key #4.\n\nThe **Hash Ratchet** specifically works by repeatedly hashing:\n```\nkey_0 = HKDF(base_secret, \"key_0\")\nkey_1 = HKDF(base_secret, \"key_1\")\n... and so on\n```\n\nThis is useful because:\n1. If someone steals key #5, they can't decrypt past messages (forward secrecy)\n2. You can skip ahead to any key number without computing all the ones before it\n\nDiscord uses the \"generation\" number in each frame to tell you which key to use.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What does \"little-endian\" mean?\u003c/summary\u003e\n\n**Endianness** is about byte order when storing multi-byte numbers.\n\nTake the number `0x12345678` (4 bytes):\n- **Big-endian**: stored as `12 34 56 78` (most significant byte first)\n- **Little-endian**: stored as `78 56 34 12` (least significant byte first)\n\nDiscord stores user IDs as little-endian 64-bit integers for the MLS context. Getting this wrong = wrong keys = decryption fails. Ask me how I know... 😅\n\u003c/details\u003e\n\n---\n\n## 3. Voice Gateway Stuff\n\n### The Connection Flow\n\nHere's what happens when you connect to voice, step by step:\n\n```\nYou                              Voice Gateway                     SFU\n │                                     │                            │\n │──── Identify (op 0) ───────────────►│                            │\n │     max_dave_protocol_version: 1    │                            │\n │                                     │                            │\n │◄─── Ready (op 2) ───────────────────│                            │\n │     ssrc, ip, port, modes           │                            │\n │                                     │                            │\n │──── Select Protocol (op 1) ────────►│                            │\n │     protocol: \"udp\", mode: ...      │                            │\n │                                     │                            │\n │◄─── Session Desc (op 4) ────────────│                            │\n │     dave_protocol_version: 1        │                            │\n │     secret_key, mode                │                            │\n │                                     │                            │\n │◄─── External Sender (op 25) ────────│  (binary)                  │\n │     credential, signature_key       │                            │\n │                                     │                            │\n │──── Key Package (op 26) ───────────►│  (binary)                  │\n │     mls_key_package                 │                            │\n │                                     │                            │\n │◄─── Proposals (op 27) ──────────────│  (binary)                  │\n │     add_proposals                   │                            │\n │                                     │                            │\n │──── Commit+Welcome (op 28) ────────►│  (binary)                  │\n │     commit, welcome                 │                            │\n │                                     │                            │\n │◄─── Announce Commit (op 29) ────────│  (binary)                  │\n │◄─── Welcome (op 30) ────────────────│  (binary)                  │\n │     welcome_for_you                 │  (the keys are MINE now)   │\n │                                     │                            │\n │──── Ready for Transition (op 23) ──►│                            │\n │                                     │                            │\n │◄─── Execute Transition (op 22) ─────│                            │\n │                                     │                            │\n ├─────────────────────────────────────┼────────────────────────────┤\n │              UDP RTP MEDIA          │                            │\n │◄────────────────────────────────────┼───────────────────────────►│\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is SSRC?\u003c/summary\u003e\n\n**SSRC (Synchronization Source Identifier)** is a 32-bit number that identifies who's sending a particular audio/video stream in RTP.\n\nEach participant in a call gets their own SSRC. When you receive an RTP packet, you check the SSRC to know who it came from. Discord tells you the SSRC-to-user mapping when people join/leave.\n\nIt's randomly generated to avoid collisions when multiple streams get mixed together.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is a WebSocket?\u003c/summary\u003e\n\n**WebSocket** is a protocol for two-way communication over a single TCP connection. Unlike HTTP (request-response), WebSocket lets either side send messages at any time.\n\nDiscord uses WebSocket for:\n- Main gateway (presence, messages, events)\n- Voice gateway (voice state, MLS messages, signaling)\n\nThe actual audio/video goes over UDP (RTP), but all the control messages go over WebSocket.\n\u003c/details\u003e\n\n### DAVE Opcodes\n\nThese are the special opcodes for E2EE stuff (21-31 are all binary format):\n\n| Code | Name                               | Direction | What It Does                        |\n|------|------------------------------------|-----------|-------------------------------------|\n| 21   | `dave_protocol_prepare_transition` | S→C       | Heads up, transition coming         |\n| 22   | `dave_protocol_execute_transition` | S→C       | OK do the transition now            |\n| 23   | `dave_protocol_ready_for_transition`| C→S      | I'm ready for the transition        |\n| 24   | `dave_protocol_prepare_epoch`      | S→C       | New epoch / group being recreated   |\n| 25   | `dave_mls_external_sender`         | S→C       | Voice gateway's credentials         |\n| 26   | `dave_mls_key_package`             | C→S       | Here's my key package               |\n| 27   | `dave_mls_proposals`               | S→C       | Add/remove proposals                |\n| 28   | `dave_mls_commit_welcome`          | C→S       | Committing with welcome attached    |\n| 29   | `dave_mls_announce_commit`         | S→C       | Broadcasting the winning commit     |\n| 30   | `dave_mls_welcome`                 | S→C       | Your welcome message                |\n| 31   | `dave_mls_invalid_commit_welcome`  | C→S       | Something's wrong with the commit   |\n\n### Binary Message Format\n\nThe binary messages look like this:\n\n```\n┌────────────────────────────────────────────┐\n│         Opcode 26 - Key Package            │\n├────────────────────────────────────────────┤\n│ uint8   opcode = 26                        │\n│ MLSMessage key_package_message             │\n│   └── KeyPackage (RFC 9420 format)         │\n└────────────────────────────────────────────┘\n\n┌────────────────────────────────────────────┐\n│       Opcode 25 - External Sender          │\n├────────────────────────────────────────────┤\n│ uint16  sequence_number                    │\n│ uint8   opcode = 25                        │\n│ SignaturePublicKey signature_key           │\n│ Credential credential                      │\n│   ├── uint16 credential_type = 1 (basic)   │\n│   └── opaque identity\u003cV\u003e                   │\n└────────────────────────────────────────────┘\n```\n\n---\n\n## 4. How Frames Get Encrypted/Decrypted\n\n### The Frame Format\n\nEvery encrypted audio/video frame looks like this:\n\n```\n╔═══════════════════════════════════════════════════════════════════════╗\n║                      DAVE ENCRYPTED FRAME FORMAT                      ║\n╠═══════════════════════════════════════════════════════════════════════╣\n║                                                                       ║\n║   0                   1                   2                   3       ║\n║   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1     ║\n║  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║  /                                                               /    ║\n║  +           Interleaved media frame (variable size)             +    ║\n║  /         (mix of unencrypted + encrypted chunks)               /    ║\n║  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║  |                                                               |    ║\n║  +              8-byte AES-GCM auth tag (truncated)              +    ║\n║  |                                                               |    ║\n║  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║  /             ULEB128 nonce (variable length)                   /    ║\n║  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║  /         ULEB128 unencrypted range pairs (variable)            /    ║\n║  +                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║  /                               | Size Byte  |  Magic 0xFAFA   |     ║\n║  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+    ║\n║                                                                       ║\n╚═══════════════════════════════════════════════════════════════════════╝\n```\n\nThe way you know something is a DAVE frame: it ends with `0xFAFA`. That's the magic marker. Very creative naming, Discord.\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is ULEB128?\u003c/summary\u003e\n\n**ULEB128 (Unsigned Little-Endian Base 128)** is a variable-length encoding for integers. Small numbers take fewer bytes, big numbers take more.\n\nHow it works:\n- Each byte uses 7 bits for data and 1 bit (the high bit) to say \"there's more\"\n- If high bit = 1, read another byte\n- If high bit = 0, you're done\n\nExamples:\n- `0x05` (5) encodes as: `05` (1 byte)\n- `0x80` (128) encodes as: `80 01` (2 bytes)\n- `0x3FFF` (16383) encodes as: `FF 7F` (2 bytes)\n\nIt's used a lot in binary formats because most numbers are small, so you save space on average.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is a Nonce?\u003c/summary\u003e\n\nA **nonce** (Number used ONCE) is a random or sequential value used exactly once with a given key. For AES-GCM:\n\n- It MUST be unique for each message encrypted with the same key\n- If you reuse a nonce with the same key, the encryption is completely broken\n- AES-GCM uses a 12-byte (96-bit) nonce\n\nDiscord uses a counter as their nonce - it goes up by 1 for each frame. The \"generation\" in the top byte tells you which key to use, and the rest is the actual counter.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What's an \"Auth Tag\"?\u003c/summary\u003e\n\nThe **Authentication Tag** (or just \"tag\") is how AES-GCM proves the data wasn't tampered with.\n\nWhen encrypting, the algorithm produces:\n1. The ciphertext (encrypted data)\n2. A tag (typically 16 bytes)\n\nWhen decrypting, you provide the tag. If the ciphertext or AAD was modified even slightly, the tag check fails and decryption is rejected.\n\nDiscord truncates this to 8 bytes to save bandwidth (voice/video generates a LOT of data). 8 bytes is still 64 bits of security, which is plenty for short-lived media frames.\n\u003c/details\u003e\n\n### The Parsing Algorithm\n\nI pulled this from `frame_processors.cpp` - here's how they parse incoming frames:\n\n```cpp\nvoid InboundFrameProcessor::ParseFrame(ArrayView\u003cuint8_t\u003e frame)\n{\n    Clear();\n\n    constexpr auto MinSupplementalBytesSize =\n        kAesGcm128TruncatedTagBytes + sizeof(SupplementalBytesSize) + sizeof(MagicMarker);\n    \n    // Step 1: Check if it's even big enough\n    if (frame.size() \u003c MinSupplementalBytesSize) return;\n\n    // Step 2: Check for the magic marker at the end\n    auto magicMarkerBuffer = frame.end() - sizeof(MagicMarker);\n    if (memcmp(magicMarkerBuffer, \u0026kMarkerBytes, sizeof(MagicMarker)) != 0) {\n        return;  // Not a DAVE frame, skip it\n    }\n\n    // Step 3: Read the supplemental bytes size (1 byte before the marker)\n    SupplementalBytesSize supplementalBytesSize;\n    auto supplementalBytesSizeBuffer = magicMarkerBuffer - sizeof(SupplementalBytesSize);\n    memcpy(\u0026supplementalBytesSize, supplementalBytesSizeBuffer, sizeof(SupplementalBytesSize));\n\n    // Step 4: Validate\n    if (frame.size() \u003c supplementalBytesSize) return;\n    if (supplementalBytesSize \u003c MinSupplementalBytesSize) return;\n\n    auto supplementalBytesBuffer = frame.end() - supplementalBytesSize;\n\n    // Step 5: Grab the 8-byte auth tag\n    tag_ = MakeArrayView(supplementalBytesBuffer, kAesGcm128TruncatedTagBytes);\n\n    // Step 6: Read the ULEB128 nonce\n    auto nonceBuffer = supplementalBytesBuffer + kAesGcm128TruncatedTagBytes;\n    auto readAt = nonceBuffer;\n    truncatedNonce_ = static_cast\u003cTruncatedSyncNonce\u003e(ReadLeb128(readAt, end));\n\n    // Step 7: Read the unencrypted ranges\n    DeserializeUnencryptedRanges(readAt, unencryptedRangesSize, unencryptedRanges_);\n\n    // Step 8: Split into authenticated data and ciphertext\n    for (const auto\u0026 range : unencryptedRanges_) {\n        auto encryptedBytes = range.offset - frameIndex;\n        if (encryptedBytes \u003e 0) {\n            AddCiphertextBytes(frame.data() + frameIndex, encryptedBytes);\n        }\n        AddAuthenticatedBytes(frame.data() + range.offset, range.size);\n        frameIndex = range.offset + range.size;\n    }\n    \n    isEncrypted_ = true;\n}\n```\n\n### The Actual Decryption (from openssl_cryptor.cpp)\n\nOnce you have all the pieces, decryption is straightforward AES-GCM:\n\n```cpp\nbool OpenSSLCryptor::Decrypt(\n    ArrayView\u003cuint8_t\u003e plaintextBufferOut,\n    ArrayView\u003cconst uint8_t\u003e ciphertextBuffer,\n    ArrayView\u003cconst uint8_t\u003e tagBuffer,\n    ArrayView\u003cconst uint8_t\u003e nonceBuffer,\n    ArrayView\u003cconst uint8_t\u003e additionalData)\n{\n    // 1. Set the nonce\n    EVP_DecryptInit_ex(cipherCtx_, nullptr, nullptr, nullptr, nonceBuffer.data());\n\n    // 2. Set AAD (the unencrypted ranges become authenticated data)\n    if (additionalData.size() \u003e 0) {\n        EVP_DecryptUpdate(cipherCtx_, nullptr, \u0026plaintextOutSize,\n            additionalData.data(), additionalData.size());\n    }\n\n    // 3. Decrypt the ciphertext\n    EVP_DecryptUpdate(cipherCtx_, plaintextBufferOut.data(), \u0026plaintextOutSize,\n        ciphertextBuffer.data(), ciphertextBuffer.size());\n\n    // 4. Set the expected tag (8 bytes truncated)\n    EVP_CIPHER_CTX_ctrl(cipherCtx_, EVP_CTRL_GCM_SET_TAG,\n        kAesGcm128TruncatedTagBytes, tagBufferCopy.data());\n\n    // 5. Verify and finalize\n    EVP_DecryptFinal_ex(cipherCtx_, plaintextBufferOut.data(), \u0026plaintextOutSize);\n    \n    return true;\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is AAD (Additional Authenticated Data)?\u003c/summary\u003e\n\n**AAD (Additional Authenticated Data)** is data that gets authenticated (protected from tampering) but NOT encrypted.\n\nIn DAVE, some parts of the frame stay unencrypted (like certain codec headers) but we still want to detect if someone messed with them. So they go into the AAD.\n\nWhen decrypting:\n- If the AAD doesn't match what was used during encryption → auth tag fails → decryption rejected\n- The AAD itself is never encrypted, just verified\n\u003c/details\u003e\n\n### Nonce Expansion\n\nThe frame only has a 4-byte truncated nonce, but AES-GCM needs 12 bytes. Here's how they expand it:\n\n```cpp\nauto nonceBuffer = std::array\u003cuint8_t, 12\u003e();\nmemset(nonceBuffer.data(), 0, 12);  // First 8 bytes are zero\nmemcpy(nonceBuffer.data() + 8, \u0026truncatedNonce, 4);  // Last 4 bytes are the nonce\n\n// Result looks like: [00 00 00 00 00 00 00 00 N3 N2 N1 N0]\n```\n\n### How They Pick Which Key To Use\n\nThe \"generation\" determines which key from the ratchet to use. It's just the top byte of the nonce:\n\n```cpp\nauto generation = cryptorManager.ComputeWrappedGeneration(\n    truncatedNonce \u003e\u003e 24  // Shift right by 24 bits = get the most significant byte\n);\n```\n\n---\n\n## 5. The Network Layer\n\n### Connection Setup\n\nHere's the basic flow in code form:\n\n```javascript\n// 1. Connect to voice WebSocket\nconst ws = new WebSocket(`wss://${endpoint}?v=8`);\n\n// 2. Send Identify - THE KEY IS max_dave_protocol_version: 1\nws.send(JSON.stringify({\n    op: 0,\n    d: {\n        server_id: guildId,\n        user_id: userId,\n        session_id: sessionId,\n        token: voiceToken,\n        video: false,\n        max_dave_protocol_version: 1  // THIS enables DAVE (the magic flag)\n    }\n}));\n\n// 3. Wait for Ready (op 2) with SSRC, IP, port\n\n// 4. Do IP Discovery over UDP\n\n// 5. Send Select Protocol\nws.send(JSON.stringify({\n    op: 1,\n    d: {\n        protocol: \"udp\",\n        data: {\n            address: discoveredIP,\n            port: discoveredPort,\n            mode: \"aead_aes256_gcm_rtpsize\"\n        },\n        codecs: [\n            { name: \"opus\", type: \"audio\", priority: 1000, payload_type: 120 }\n        ]\n    }\n}));\n\n// 6. Receive Session Description (op 4) with secret_key and dave_protocol_version\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is IP Discovery?\u003c/summary\u003e\n\n**IP Discovery** (sometimes called STUN-like discovery) is how you figure out your public IP and port when you're behind NAT.\n\nDiscord's version:\n1. Send a special packet to their server with your SSRC\n2. Server sees what IP:port it came from (your public address)\n3. Server sends that info back to you\n4. You tell Discord \"hey, send my audio to this address\"\n\nThis is necessary because your computer often doesn't know its own public IP (it just knows its local 192.168.x.x address).\n\u003c/details\u003e\n\n### RTP Packet Structure\n\n```\n╔══════════════════════════════════════════════════════════════════╗\n║                        RTP PACKET FORMAT                         ║\n╠══════════════════════════════════════════════════════════════════╣\n║                                                                  ║\n║  ┌──────────────────────────────────────────────────────────┐    ║\n║  │             RTP HEADER (12 bytes, unencrypted)           │    ║\n║  ├──────────────────────────────────────────────────────────┤    ║\n║  │  Version + Flags │ Payload Type │   Sequence Number      │    ║\n║  │     (1 byte)     │   (1 byte)   │     (2 bytes)          │    ║\n║  ├──────────────────────────────────────────────────────────┤    ║\n║  │                   Timestamp (4 bytes)                    │    ║\n║  ├──────────────────────────────────────────────────────────┤    ║\n║  │                      SSRC (4 bytes)                      │    ║\n║  └──────────────────────────────────────────────────────────┘    ║\n║                                                                  ║\n║  ┌──────────────────────────────────────────────────────────┐    ║\n║  │                    ENCRYPTED PAYLOAD                     │    ║\n║  │   (Transport encrypted first, then DAVE E2EE inside)     │    ║\n║  │   (it's like an encryption onion... shrek would approve) │    ║\n║  └──────────────────────────────────────────────────────────┘    ║\n║                                                                  ║\n║  ┌──────────────────────────────────────────────────────────┐    ║\n║  │              Transport Nonce (4 bytes)                   │    ║\n║  └──────────────────────────────────────────────────────────┘    ║\n║                                                                  ║\n╚══════════════════════════════════════════════════════════════════╝\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is RTP?\u003c/summary\u003e\n\n**RTP (Real-time Transport Protocol)** is the standard for streaming audio/video over the internet. Pretty much every VoIP/video call uses it.\n\nThe header contains:\n- **Sequence number**: detect packet loss and reorder\n- **Timestamp**: sync audio/video timing\n- **SSRC**: identify who's sending\n- **Payload type**: what codec is being used\n\nRTP itself doesn't provide encryption - that's what SRTP and DAVE add on top.\n\u003c/details\u003e\n\n### The Two Decryption Layers\n\nWhen you receive a packet, you gotta decrypt it twice:\n\n```\n         RECEIVE RTP PACKET\n                │\n                ▼\n┌──────────────────────────────────────┐\n│   LAYER 1: Transport Decryption      │\n│   Mode: aead_aes256_gcm_rtpsize      │\n│   Key: secret_key from Session Desc  │\n│   This removes Discord's encryption  │\n└──────────────────────────────────────┘\n                │\n                ▼\n┌──────────────────────────────────────┐\n│      LAYER 2: DAVE Decryption        │\n│   Check for magic marker 0xFAFA      │\n│   Parse the supplemental data        │\n│   Get sender key from MLS group      │\n│   AES-128-GCM decrypt                │\n└──────────────────────────────────────┘\n                │\n                ▼\n┌──────────────────────────────────────┐\n│         LAYER 3: Codec               │\n│   Opus decode for audio              │\n│   H264/VP8/VP9/AV1 for video         │\n└──────────────────────────────────────┘\n                │\n                ▼\n        RAW PCM / RAW VIDEO\n           (profit???)\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is Opus?\u003c/summary\u003e\n\n**Opus** is an audio codec designed for real-time communication. Discord uses it for all voice chat.\n\nWhy Opus is great:\n- Low latency (important for real-time)\n- Works well at many bitrates (6 kbps to 510 kbps)\n- Handles both speech and music well\n- Open standard, royalty-free\n\nDiscord typically uses 48kHz sample rate, stereo, at around 64-128 kbps.\n\u003c/details\u003e\n\n---\n\n## 6. System Architecture\n\n### What I Need To Build\n\n```\n╔══════════════════════════════════════════════════════════════════════╗\n║                     VOICE DECRYPTION ARCHITECTURE                    ║\n╠══════════════════════════════════════════════════════════════════════╣\n║                                                                      ║\n║   ┌──────────────────────────────────────────────────────────────┐   ║\n║   │                      APPLICATION LAYER                       │   ║\n║   │  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐  │   ║\n║   │  │  Discord  │  │    MLS    │  │   DAVE    │  │   Audio   │  │   ║\n║   │  │  Gateway  │  │  Session  │  │ Decryptor │  │  Handler  │  │   ║\n║   │  │  Client   │  │  Manager  │  │           │  │           │  │   ║\n║   │  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  │   ║\n║   └────────┼──────────────┼──────────────┼──────────────┼────────┘   ║\n║            │              │              │              │            ║\n║   ┌────────┼──────────────┼──────────────┼──────────────┼────────┐   ║\n║   │        │         TRANSPORT LAYER     │              │        │   ║\n║   │  ┌─────▼─────┐  ┌─────▼─────┐  ┌─────▼─────┐  ┌─────▼─────┐  │   ║\n║   │  │   Voice   │  │    UDP    │  │ Transport │  │   Opus    │  │   ║\n║   │  │  Gateway  │  │   Socket  │  │ Decryptor │  │  Decoder  │  │   ║\n║   │  │ WebSocket │  │   (RTP)   │  │  (SRTP)   │  │           │  │   ║\n║   │  └───────────┘  └───────────┘  └───────────┘  └───────────┘  │   ║\n║   └─────────────────────────────────────────────────────────────-┘   ║\n║                                                                      ║\n║   ┌─────────────────────────────────────────────────────────────┐    ║\n║   │                       CRYPTO LAYER                          │    ║\n║   │  ┌───────────┐      ┌───────────┐      ┌───────────┐        │    ║\n║   │  │  ts-mls   │      │  AES-GCM  │      │    Key    │        │    ║\n║   │  │   (MLS)   │      │  (crypto) │      │  Ratchet  │        │    ║\n║   │  └───────────┘      └───────────┘      └───────────┘        │    ║\n║   └─────────────────────────────────────────────────────────────┘    ║\n║                                                                      ║\n╚══════════════════════════════════════════════════════════════════════╝\n```\n\n### Data Flow (How Audio Gets Processed)\n\n```\n╔═══════════════════════════════════════════════════════════════════╗\n║                          INCOMING AUDIO FLOW                      ║\n╠═══════════════════════════════════════════════════════════════════╣\n║                                                                   ║\n║   UDP Socket                                                      ║\n║       │                                                           ║\n║       ▼                                                           ║\n║   ┌────────────────┐                                              ║\n║   │  RTP Packet    │                                              ║\n║   │  (Encrypted)   │                                              ║\n║   └───────┬────────┘                                              ║\n║           │                                                       ║\n║           ▼                                                       ║\n║   ┌────────────────┐       ┌─────────────────────┐                ║\n║   │   Transport    │──────►│  secret_key from    │                ║\n║   │   Decrypt      │       │  Session Desc       │                ║\n║   └───────┬────────┘       └─────────────────────┘                ║\n║           │                                                       ║\n║           ▼                                                       ║\n║   ┌────────────────┐       ┌─────────────────────────────────┐    ║\n║   │  DAVE Frame    │       │  Does it end with 0xFAFA?       │    ║\n║   │  Detection     │──────►│  Yes → E2EE frame               │    ║\n║   └───────┬────────┘       │  No → Passthrough/silence       │    ║\n║           │                └─────────────────────────────────┘    ║\n║           ▼                                                       ║\n║   ┌────────────────┐       ┌─────────────────────────────────┐    ║\n║   │  Parse Frame   │       │  Extract:                       │    ║\n║   │  Supplemental  │──────►│   - 8-byte auth tag             │    ║\n║   └───────┬────────┘       │   - ULEB128 nonce               │    ║\n║           │                │   - Unencrypted ranges          │    ║\n║           │                └─────────────────────────────────┘    ║\n║           ▼                                                       ║\n║   ┌────────────────┐       ┌─────────────────────────────────┐    ║\n║   │  Get Sender    │       │  sender_key = MLS.export(       │    ║\n║   │  Key from MLS  │──────►│    \"Discord Secure Frames v0\",  │    ║\n║   └───────┬────────┘       │    little_endian(user_id),      │    ║\n║           │                │    16                           │    ║\n║           │                │  ).ratchet(generation)          │    ║\n║           │                └─────────────────────────────────┘    ║\n║           ▼                                                       ║\n║   ┌────────────────┐       ┌─────────────────────────────────┐    ║\n║   │  AES-128-GCM   │       │  plaintext = decrypt(           │    ║\n║   │  Decrypt       │──────►│    key: sender_key,             │    ║\n║   └───────┬────────┘       │    nonce: expand(trunc_nonce),  │    ║\n║           │                │    aad: unencrypted_data,       │    ║\n║           │                │    ciphertext: encrypted_data,  │    ║\n║           │                │    tag: truncated_tag           │    ║\n║           │                │  )                              │    ║\n║           │                └─────────────────────────────────┘    ║\n║           ▼                                                       ║\n║   ┌────────────────┐                                              ║\n║   │  Reconstruct   │       Put unencrypted + decrypted parts      ║\n║   │  Original      │       back in their original positions       ║\n║   └───────┬────────┘                                              ║\n║           │                                                       ║\n║           ▼                                                       ║\n║   ┌────────────────┐                                              ║\n║   │  Opus Decode   │       opus_decode(frame) → PCM samples       ║\n║   └───────┬────────┘                                              ║\n║           │                                                       ║\n║           ▼                                                       ║\n║   ┌────────────────┐                                              ║\n║   │  Output/Save   │       WAV / PCM / whatever you want          ║\n║   └────────────────┘                                              ║\n║                                                                   ║\n╚═══════════════════════════════════════════════════════════════════╝\n```\n\n---\n\n## 7. What I Found In libdave's Source Code\n\n\u003cdetails\u003e\n\u003csummary\u003e🤔 What is libdave?\u003c/summary\u003e\n\n**libdave** is Discord's official open-source implementation of the DAVE protocol. They published it on GitHub at `github.com/discord/libdave`.\n\nIt has:\n- **C++ implementation**: The core library, uses OpenSSL and mlspp\n- **JavaScript/WASM build**: Same C++ code compiled to WebAssembly for browser use\n- **Protocol documentation**: Though not super detailed, it helped me figure things out\n\nI spent a lot of time reading through the C++ source to understand exactly how encryption/decryption works.\n\u003c/details\u003e\n\n### Constants (from common.h)\n\nThese are the magic numbers they use:\n\n```cpp\n// Sizes\nconstexpr size_t kAesGcm128KeyBytes = 16;\nconstexpr size_t kAesGcm128NonceBytes = 12;\nconstexpr size_t kAesGcm128TruncatedSyncNonceBytes = 4;\nconstexpr size_t kAesGcm128TruncatedSyncNonceOffset = 8;  // Where in the 12-byte nonce to put the 4 bytes\nconstexpr size_t kAesGcm128TruncatedTagBytes = 8;\nconstexpr size_t kRatchetGenerationBytes = 1;\nconstexpr size_t kRatchetGenerationShiftBits = 24;  // 8 * (4 - 1)\n\n// Magic marker\nconstexpr MagicMarker kMarkerBytes = 0xFAFA;\n\n// Timing stuff\nconstexpr auto kCryptorExpiry = std::chrono::seconds(10);\n\n// Behavior\nconstexpr auto kMaxGenerationGap = 250;\nconstexpr auto kMaxMissingNonces = 1000;\nconstexpr auto kGenerationWrap = 256;  // 1 \u003c\u003c 8\n\n// Opus silence (sent when someone stops talking)\nconstexpr std::array\u003cuint8_t, 3\u003e kOpusSilencePacket = {0xF8, 0xFF, 0xFE};\n```\n\n### ULEB128 Encoding\n\nThey use ULEB128 for variable-length integers. Here's the read function:\n\n```cpp\nsize_t ReadLeb128(const uint8_t*\u0026 readAt, const uint8_t* end) {\n    size_t value = 0;\n    size_t shift = 0;\n    \n    while (readAt \u003c end) {\n        uint8_t byte = *readAt++;\n        value |= (byte \u0026 0x7F) \u003c\u003c shift;\n        if ((byte \u0026 0x80) == 0) break;  // High bit not set = last byte\n        shift += 7;\n    }\n    \n    return value;\n}\n```\n\n### Key Ratchet (from mls_key_ratchet.cpp)\n\n```cpp\nMlsKeyRatchet::MlsKeyRatchet(::mlspp::CipherSuite suite, bytes baseSecret) noexcept\n    : hashRatchet_(suite, std::move(baseSecret))\n{\n}\n\nEncryptionKey MlsKeyRatchet::GetKey(KeyGeneration generation) noexcept\n{\n    try {\n        auto keyAndNonce = hashRatchet_.get(generation);\n        return std::move(keyAndNonce.key.as_vec());\n    }\n    catch (const std::exception\u0026 e) {\n        return {};\n    }\n}\n```\n\n---\n\n## 8. Libraries I'll Need\n\n### For a Node.js Implementation\n\n```json\n{\n  \"dependencies\": {\n    \"ts-mls\": \"^1.0.0\",           // MLS protocol implementation\n    \"@noble/curves\": \"^1.0.0\",    // P-256 curve for the ciphersuite\n    \"ws\": \"^8.0.0\",               // WebSocket client\n    \"@discordjs/opus\": \"^0.9.0\",  // Opus codec (or opusscript for pure JS)\n  }\n}\n```\n\nBuilt-in Node stuff I'll use:\n- `crypto` - for AES-GCM\n- `dgram` - for UDP\n\n### Alternative: Use Discord's WASM Build\n\nDiscord actually published their libdave as WebAssembly too:\n- Located at `github.com/discord/libdave/js/wasm/`\n- Compiled from C++ via `bindings_wasm.cpp`\n- Would give native DAVE frame processing without reimplementing everything\n\n---\n\n## 9. Code Examples\n\n### Voice Gateway Connection\n\n```typescript\nimport WebSocket from 'ws';\n\nclass VoiceGateway {\n    private ws: WebSocket;\n    private ssrc: number;\n    private secretKey: Uint8Array;\n    private daveProtocolVersion: number;\n    \n    async connect(config: VoiceGatewayConfig): Promise\u003cvoid\u003e {\n        this.ws = new WebSocket(`wss://${config.endpoint}?v=8`);\n        \n        this.ws.on('open', () =\u003e {\n            // Identify with DAVE enabled\n            this.send({\n                op: 0,\n                d: {\n                    server_id: config.serverId,\n                    user_id: config.userId,\n                    session_id: config.sessionId,\n                    token: config.token,\n                    video: false,\n                    max_dave_protocol_version: 1  // \u003c-- This is the magic flag\n                }\n            });\n        });\n        \n        this.ws.on('message', (data) =\u003e this.handleMessage(data));\n    }\n    \n    private handleMessage(data: Buffer): void {\n        // Binary messages (opcodes 21-31) are DAVE stuff\n        if (data[0] \u003e= 21 \u0026\u0026 data[0] \u003c= 31) {\n            this.handleBinaryMessage(data);\n            return;\n        }\n        \n        const msg = JSON.parse(data.toString());\n        switch (msg.op) {\n            case 2: this.handleReady(msg.d); break;\n            case 4: this.handleSessionDescription(msg.d); break;\n        }\n    }\n    \n    private handleBinaryMessage(data: Buffer): void {\n        const opcode = data[0];\n        switch (opcode) {\n            case 25: this.handleExternalSender(data); break;\n            case 27: this.handleProposals(data); break;\n            case 29: this.handleAnnounceCommit(data); break;\n            case 30: this.handleWelcome(data); break; // The good stuff arrives here\n        }\n    }\n}\n```\n\n### MLS Session Management\n\n```typescript\nimport { \n    createGroup, generateKeyPackage, joinGroup, processMessage,\n    getCiphersuiteImpl, getCiphersuiteFromName\n} from 'ts-mls';\n\nclass DaveMlsSession {\n    private mlsState: any;\n    private senderKeyRatchets: Map\u003cstring, KeyRatchet\u003e = new Map();\n    \n    async init(userId: string): Promise\u003cUint8Array\u003e {\n        // Get the right ciphersuite (P-256 + AES-128-GCM + SHA-256)\n        const impl = await getCiphersuiteImpl(\n            getCiphersuiteFromName(\"MLS_128_DHKEMP256_AES128GCM_SHA256_P256\")\n        );\n        \n        // Credential is just your user ID as bytes\n        const credential = {\n            credentialType: 1,  // basic\n            identity: this.userIdToBytes(userId)\n        };\n        \n        // Generate and return the key package\n        const keyPkg = await generateKeyPackage({\n            credential,\n            cipherSuite: impl\n        });\n        \n        return keyPkg.publicPackage;\n    }\n    \n    async processWelcome(welcomeData: Uint8Array): Promise\u003cvoid\u003e {\n        // Join the group using the welcome message\n        this.mlsState = await joinGroup({\n            context: this.context,\n            welcome: welcomeData,\n            keyPackage: this.keyPackage,\n            privateKeys: this.privateKeys\n        });\n    }\n    \n    getSenderKey(senderUserId: string, generation: number): Uint8Array {\n        const label = \"Discord Secure Frames v0\";\n        const context = this.userIdToLittleEndian(senderUserId);\n        \n        const baseSecret = this.mlsState.export(label, context, 16);\n        \n        let ratchet = this.senderKeyRatchets.get(senderUserId);\n        if (!ratchet) {\n            ratchet = new KeyRatchet(baseSecret);\n            this.senderKeyRatchets.set(senderUserId, ratchet);\n        }\n        \n        return ratchet.getKey(generation);\n    }\n    \n    private userIdToLittleEndian(userId: string): Uint8Array {\n        const id = BigInt(userId);\n        const buffer = new ArrayBuffer(8);\n        const view = new DataView(buffer);\n        view.setBigUint64(0, id, true);  // little-endian!\n        return new Uint8Array(buffer);\n    }\n}\n```\n\n### DAVE Frame Decryption\n\n```typescript\nimport { createDecipheriv } from 'crypto';\n\nconst MAGIC_MARKER = 0xFAFA;\nconst TAG_SIZE = 8;\nconst MARKER_SIZE = 2;\nconst SIZE_BYTE = 1;\nconst MIN_SUPPLEMENTAL = TAG_SIZE + SIZE_BYTE + MARKER_SIZE;  // 11 bytes\n\nclass DaveDecryptor {\n    private mlsSession: DaveMlsSession;\n    \n    parseFrame(frame: Uint8Array): ParsedFrame | null {\n        if (frame.length \u003c MIN_SUPPLEMENTAL) return null;\n        \n        // Check magic marker at the end\n        const markerOffset = frame.length - MARKER_SIZE;\n        const marker = (frame[markerOffset] \u003c\u003c 8) | frame[markerOffset + 1];\n        if (marker !== MAGIC_MARKER) return null;\n        \n        // Read supplemental size\n        const sizeOffset = markerOffset - SIZE_BYTE;\n        const supplementalSize = frame[sizeOffset];\n        \n        if (frame.length \u003c supplementalSize) return null;\n        \n        const supplementalStart = frame.length - supplementalSize;\n        \n        // Grab the 8-byte tag\n        const tag = frame.slice(supplementalStart, supplementalStart + TAG_SIZE);\n        \n        // Read ULEB128 nonce\n        let readPos = supplementalStart + TAG_SIZE;\n        const { value: nonce, bytesRead } = this.readULEB128(frame, readPos);\n        readPos += bytesRead;\n        \n        // Read unencrypted ranges\n        const ranges: Array\u003c{offset: number, size: number}\u003e = [];\n        while (readPos \u003c sizeOffset) {\n            const { value: offset, bytesRead: b1 } = this.readULEB128(frame, readPos);\n            readPos += b1;\n            const { value: size, bytesRead: b2 } = this.readULEB128(frame, readPos);\n            readPos += b2;\n            ranges.push({ offset, size });\n        }\n        \n        // Split into authenticated and ciphertext\n        const actualFrameSize = frame.length - supplementalSize;\n        const authenticated: number[] = [];\n        const ciphertext: number[] = [];\n        let frameIdx = 0;\n        \n        for (const range of ranges) {\n            if (range.offset \u003e frameIdx) {\n                for (let i = frameIdx; i \u003c range.offset; i++) {\n                    ciphertext.push(frame[i]);\n                }\n            }\n            for (let i = range.offset; i \u003c range.offset + range.size; i++) {\n                authenticated.push(frame[i]);\n            }\n            frameIdx = range.offset + range.size;\n        }\n        \n        if (frameIdx \u003c actualFrameSize) {\n            for (let i = frameIdx; i \u003c actualFrameSize; i++) {\n                ciphertext.push(frame[i]);\n            }\n        }\n        \n        return {\n            tag, nonce,\n            unencryptedRanges: ranges,\n            authenticatedData: new Uint8Array(authenticated),\n            ciphertext: new Uint8Array(ciphertext)\n        };\n    }\n    \n    decrypt(senderUserId: string, parsed: ParsedFrame): Uint8Array | null {\n        // Generation is the top byte of the nonce\n        const generation = parsed.nonce \u003e\u003e\u003e 24;\n        \n        // Get the key\n        const senderKey = this.mlsSession.getSenderKey(senderUserId, generation);\n        if (!senderKey) return null;\n        \n        // Expand 4-byte nonce to 12 bytes\n        const fullNonce = new Uint8Array(12);\n        fullNonce[8] = (parsed.nonce \u003e\u003e\u003e 24) \u0026 0xFF;\n        fullNonce[9] = (parsed.nonce \u003e\u003e\u003e 16) \u0026 0xFF;\n        fullNonce[10] = (parsed.nonce \u003e\u003e\u003e 8) \u0026 0xFF;\n        fullNonce[11] = parsed.nonce \u0026 0xFF;\n        \n        try {\n            const decipher = createDecipheriv('aes-128-gcm', senderKey, fullNonce);\n            decipher.setAAD(parsed.authenticatedData);\n            \n            // Pad tag to 16 bytes\n            const fullTag = new Uint8Array(16);\n            fullTag.set(parsed.tag, 0);\n            decipher.setAuthTag(fullTag);\n            \n            const plaintext = decipher.update(parsed.ciphertext);\n            decipher.final();\n            \n            return new Uint8Array(plaintext);\n        } catch (e) {\n            return null;  // Decryption failed, probably wrong key\n        }\n    }\n}\n```\n\n### Audio Handling\n\n```typescript\nimport { OpusDecoder } from '@discordjs/opus';\nimport * as fs from 'fs';\n\nclass AudioHandler {\n    private opusDecoder: OpusDecoder;\n    private outputStream: fs.WriteStream;\n    private sampleRate = 48000;\n    private channels = 2;\n    \n    constructor(outputPath: string) {\n        this.opusDecoder = new OpusDecoder(this.sampleRate, this.channels);\n        this.outputStream = fs.createWriteStream(outputPath);\n        this.writeWavHeader();\n    }\n    \n    processFrame(opusFrame: Uint8Array): void {\n        const pcm = this.opusDecoder.decode(Buffer.from(opusFrame));\n        this.outputStream.write(pcm);\n    }\n    \n    private writeWavHeader(): void {\n        const header = Buffer.alloc(44);\n        header.write('RIFF', 0);\n        header.writeUInt32LE(0, 4);  // File size (fill in later)\n        header.write('WAVE', 8);\n        header.write('fmt ', 12);\n        header.writeUInt32LE(16, 16);\n        header.writeUInt16LE(1, 20);   // PCM\n        header.writeUInt16LE(this.channels, 22);\n        header.writeUInt32LE(this.sampleRate, 24);\n        header.writeUInt32LE(this.sampleRate * this.channels * 2, 28);\n        header.writeUInt16LE(this.channels * 2, 32);\n        header.writeUInt16LE(16, 34);\n        header.write('data', 36);\n        header.writeUInt32LE(0, 40);  // Data size (fill in later)\n        \n        this.outputStream.write(header);\n    }\n    \n    finalize(): void {\n        const fileSize = this.outputStream.bytesWritten;\n        const dataSize = fileSize - 44;\n        \n        const fd = fs.openSync(this.outputStream.path as string, 'r+');\n        const buf = Buffer.alloc(4);\n        \n        buf.writeUInt32LE(fileSize - 8, 0);\n        fs.writeSync(fd, buf, 0, 4, 4);\n        \n        buf.writeUInt32LE(dataSize, 0);\n        fs.writeSync(fd, buf, 0, 4, 40);\n        \n        fs.closeSync(fd);\n        this.outputStream.end();\n    }\n}\n```\n\n---\n\n## Final Notes\n\nEverything here came from:\n- Discord's open-source `libdave` repo\n- The `dave-protocol` whitepaper at daveprotocol.com  \n- Reverse engineering the voice gateway messages\n- Reading through the RFC 9420 (MLS) spec\n- A lot of caffeine and questionable life choices\n\nThe main blocker right now is getting the MLS implementation right. The `ts-mls` library exists but isn't designed specifically for Discord's usage. I might need to do some adapting or just use Discord's WASM build directly. Will update the repo when I figure that part out.\n\nBut the key insight is: **as a legitimate participant in the call, I get the encryption keys**. I'm not trying to \"break\" the encryption - Discord literally hands me the keys because I'm supposed to be there. That's just how E2EE works lol.\n\n---\n\n*Last updated: when I finally understood what ULEB128 was 💀*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fofficial-alex%2Flibundave","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fofficial-alex%2Flibundave","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fofficial-alex%2Flibundave/lists"}