Skip to content
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

feat(voice): add aead_xchacha20_poly1305_rtpsize encryption mode, remove old modes #1228

Merged
merged 9 commits into from
Nov 13, 2024
1 change: 1 addition & 0 deletions changelog/1228.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for ``aead_xchacha20_poly1305_rtpsize`` encryption mode for voice connections, and remove deprecated ``xsalsa20_poly1305*`` modes.
1 change: 1 addition & 0 deletions changelog/1228.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise PyNaCl version requirement to ``v1.5.0``.
2 changes: 1 addition & 1 deletion disnake/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ async def initial_connection(self, data: VoiceReadyPayload) -> None:
state.port = struct.unpack_from(">H", recv, len(recv) - 2)[0]
_log.debug("detected ip: %s port: %s", state.ip, state.port)

# there *should* always be at least one supported mode (xsalsa20_poly1305)
# there *should* always be at least one supported mode
modes: List[SupportedModes] = [
mode for mode in data["modes"] if mode in self._connection.supported_modes
]
Expand Down
5 changes: 4 additions & 1 deletion disnake/types/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
from .member import MemberWithUser
from .snowflake import Snowflake

SupportedModes = Literal["xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305"]
SupportedModes = Literal[
# "aead_aes256_gcm_rtpsize", # supported in libsodium, but not exposed by pynacl
"aead_xchacha20_poly1305_rtpsize",
]


class _VoiceState(TypedDict):
Expand Down
43 changes: 19 additions & 24 deletions disnake/voice_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,7 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None:
self.ws: DiscordVoiceWebSocket = MISSING

warn_nacl = not has_nacl
supported_modes: Tuple[SupportedModes, ...] = (
"xsalsa20_poly1305_lite",
"xsalsa20_poly1305_suffix",
"xsalsa20_poly1305",
)
supported_modes: Tuple[SupportedModes, ...] = ("aead_xchacha20_poly1305_rtpsize",)

@property
def guild(self) -> Guild:
Expand Down Expand Up @@ -512,36 +508,35 @@ def _get_voice_packet(self, data):
header = bytearray(12)

# Formulate rtp header
header[0] = 0x80
header[1] = 0x78
header[0] = 0x80 # version = 2
header[1] = 0x78 # payload type = 120 (opus)
struct.pack_into(">H", header, 2, self.sequence)
struct.pack_into(">I", header, 4, self.timestamp)
struct.pack_into(">I", header, 8, self.ssrc)

encrypt_packet = getattr(self, f"_encrypt_{self.mode}")
return encrypt_packet(header, data)

def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes:
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = bytearray(24)
nonce[:12] = header
def _get_nonce(self, pad: int):
# returns (nonce, padded_nonce).
# n.b. all currently implemented modes use the same nonce size (192 bits / 24 bytes)
nonce = struct.pack(">I", self._lite_nonce)

return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
self._lite_nonce += 1
if self._lite_nonce > 4294967295:
self._lite_nonce = 0

def _encrypt_xsalsa20_poly1305_suffix(self, header: bytes, data) -> bytes:
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
return (nonce, nonce.ljust(pad, b"\0"))

return header + box.encrypt(bytes(data), nonce).ciphertext + nonce
def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes:
box = nacl.secret.Aead(bytes(self.secret_key))
nonce, padded_nonce = self._get_nonce(nacl.secret.Aead.NONCE_SIZE)

def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes:
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = bytearray(24)

nonce[:4] = struct.pack(">I", self._lite_nonce)
self.checked_add("_lite_nonce", 1, 4294967295)

return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]
return (
header
+ box.encrypt(bytes(data), aad=bytes(header), nonce=padded_nonce).ciphertext
+ nonce
)

def play(
self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ speed = [
'cchardet; python_version < "3.10"',
]
voice = [
"PyNaCl>=1.3.0,<1.6",
"PyNaCl>=1.5.0,<1.6",
]
docs = [
"sphinx==7.0.1",
Expand Down
Loading