From a0a5f846880a3d6312e5ffbabff8f6ceb4e0096d Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 13 Aug 2024 21:24:19 +0200 Subject: [PATCH 1/8] feat(voice): add `aead_xchacha20_poly1305_rtpsize`, remove 2/3 old modes --- disnake/gateway.py | 2 +- disnake/types/voice.py | 10 +++++++++- disnake/voice_client.py | 27 ++++++++++++++------------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/disnake/gateway.py b/disnake/gateway.py index 97508574c3..b42414b924 100644 --- a/disnake/gateway.py +++ b/disnake/gateway.py @@ -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 ] diff --git a/disnake/types/voice.py b/disnake/types/voice.py index 8a7d4870a0..481c8ce7fc 100644 --- a/disnake/types/voice.py +++ b/disnake/types/voice.py @@ -7,7 +7,15 @@ from .member import MemberWithUser from .snowflake import Snowflake -SupportedModes = Literal["xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305"] +SupportedModes = Literal[ + "aead_aes256_gcm_rtpsize", + "aead_xchacha20_poly1305_rtpsize", + "xsalsa20_poly1305_lite_rtpsize", + "aead_aes256_gcm", + "xsalsa20_poly1305", + "xsalsa20_poly1305_suffix", + "xsalsa20_poly1305_lite", +] class _VoiceState(TypedDict): diff --git a/disnake/voice_client.py b/disnake/voice_client.py index a6cc13e0ba..9a29258dab 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -229,9 +229,9 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: warn_nacl = not has_nacl supported_modes: Tuple[SupportedModes, ...] = ( + # "aead_aes256_gcm_rtpsize", # supported in libsodium, but not exposed by pynacl + "aead_xchacha20_poly1305_rtpsize", "xsalsa20_poly1305_lite", - "xsalsa20_poly1305_suffix", - "xsalsa20_poly1305", ) @property @@ -512,8 +512,8 @@ 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) @@ -521,18 +521,19 @@ def _get_voice_packet(self, data): 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)) + def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: + box = nacl.secret.Aead(bytes(self.secret_key)) nonce = bytearray(24) - nonce[:12] = header - - return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext - 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) + nonce[:4] = struct.pack(">I", self._lite_nonce) + # TODO: simplify this + self.checked_add("_lite_nonce", 1, 4294967295) - return header + box.encrypt(bytes(data), nonce).ciphertext + nonce + return ( + header + + box.encrypt(bytes(data), aad=bytes(header), nonce=bytes(nonce)).ciphertext + + nonce[:4] + ) def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: box = nacl.secret.SecretBox(bytes(self.secret_key)) From 86571170556a759f12144eadb35feb93eb54151d Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 13 Aug 2024 21:48:03 +0200 Subject: [PATCH 2/8] refactor: mvoe common nonce logic to separate method no call to `checked_add` because once function call is already expensive enough, and it doesn't get much more simple than that anyway --- disnake/voice_client.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/disnake/voice_client.py b/disnake/voice_client.py index 9a29258dab..7c447f08b5 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -521,28 +521,29 @@ def _get_voice_packet(self, data): encrypt_packet = getattr(self, f"_encrypt_{self.mode}") return encrypt_packet(header, data) - def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: - box = nacl.secret.Aead(bytes(self.secret_key)) + def _get_nonce(self) -> bytes: nonce = bytearray(24) - nonce[:4] = struct.pack(">I", self._lite_nonce) - # TODO: simplify this - self.checked_add("_lite_nonce", 1, 4294967295) + + self._lite_nonce += 1 + if self._lite_nonce > 4294967295: + self._lite_nonce = 0 + + return bytes(nonce) + + def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: + box = nacl.secret.Aead(bytes(self.secret_key)) + nonce = self._get_nonce() return ( - header - + box.encrypt(bytes(data), aad=bytes(header), nonce=bytes(nonce)).ciphertext - + nonce[:4] + header + box.encrypt(bytes(data), aad=bytes(header), nonce=nonce).ciphertext + nonce[:4] ) 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) + nonce = self._get_nonce() - return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] + return header + box.encrypt(bytes(data), nonce).ciphertext + nonce[:4] def play( self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None From d20cea77946a75e5929f6447c30d8cae34e854ac Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 13 Aug 2024 22:04:30 +0200 Subject: [PATCH 3/8] docs: add changelog entry --- changelog/1228.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1228.feature.rst diff --git a/changelog/1228.feature.rst b/changelog/1228.feature.rst new file mode 100644 index 0000000000..707acfaca7 --- /dev/null +++ b/changelog/1228.feature.rst @@ -0,0 +1 @@ +Add support for ``aead_xchacha20_poly1305_rtpsize`` encryption mode for voice connections, and remove deprecated ``xsalsa20_poly1305*`` modes (except ``xsalsa20_poly1305_lite``). From 2fe6946c15920deeedb31a7092016087b282503d Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 14 Aug 2024 20:57:48 +0200 Subject: [PATCH 4/8] perf: avoid bytearray `x.ljust(24, b"\0")` is surprisingly fast, it's more performant than `x + bytes(20)` and about 3x as fast as the previous `bytearray` construction --- disnake/voice_client.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/disnake/voice_client.py b/disnake/voice_client.py index 7c447f08b5..e3a27519cb 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -521,29 +521,32 @@ def _get_voice_packet(self, data): encrypt_packet = getattr(self, f"_encrypt_{self.mode}") return encrypt_packet(header, data) - def _get_nonce(self) -> bytes: - nonce = bytearray(24) - nonce[:4] = struct.pack(">I", self._lite_nonce) + 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) self._lite_nonce += 1 if self._lite_nonce > 4294967295: self._lite_nonce = 0 - return bytes(nonce) + return (nonce, nonce.ljust(pad, b"\0")) def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: box = nacl.secret.Aead(bytes(self.secret_key)) - nonce = self._get_nonce() + nonce, padded_nonce = self._get_nonce(nacl.secret.Aead.NONCE_SIZE) return ( - header + box.encrypt(bytes(data), aad=bytes(header), nonce=nonce).ciphertext + nonce[:4] + header + + box.encrypt(bytes(data), aad=bytes(header), nonce=padded_nonce).ciphertext + + nonce ) def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: box = nacl.secret.SecretBox(bytes(self.secret_key)) - nonce = self._get_nonce() + nonce, padded_nonce = self._get_nonce(nacl.secret.SecretBox.NONCE_SIZE) - return header + box.encrypt(bytes(data), nonce).ciphertext + nonce[:4] + return header + box.encrypt(bytes(data), padded_nonce).ciphertext + nonce def play( self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None From a33414234210ed65f16f9187718fbd6d9ef205d5 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 14 Aug 2024 21:21:37 +0200 Subject: [PATCH 5/8] fix(types): remove unsupported modes --- disnake/types/voice.py | 6 +----- disnake/voice_client.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/disnake/types/voice.py b/disnake/types/voice.py index 481c8ce7fc..3377d7ba55 100644 --- a/disnake/types/voice.py +++ b/disnake/types/voice.py @@ -8,12 +8,8 @@ from .snowflake import Snowflake SupportedModes = Literal[ - "aead_aes256_gcm_rtpsize", + # "aead_aes256_gcm_rtpsize", # supported in libsodium, but not exposed by pynacl "aead_xchacha20_poly1305_rtpsize", - "xsalsa20_poly1305_lite_rtpsize", - "aead_aes256_gcm", - "xsalsa20_poly1305", - "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", ] diff --git a/disnake/voice_client.py b/disnake/voice_client.py index e3a27519cb..565fcf4448 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -229,7 +229,6 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: warn_nacl = not has_nacl supported_modes: Tuple[SupportedModes, ...] = ( - # "aead_aes256_gcm_rtpsize", # supported in libsodium, but not exposed by pynacl "aead_xchacha20_poly1305_rtpsize", "xsalsa20_poly1305_lite", ) From 5f3d2db660afd6906d2744e277c087e6e8da56ab Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 14 Aug 2024 21:33:12 +0200 Subject: [PATCH 6/8] chore: remove unnecessary info from changelog --- changelog/1228.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/1228.feature.rst b/changelog/1228.feature.rst index 707acfaca7..5457283ab9 100644 --- a/changelog/1228.feature.rst +++ b/changelog/1228.feature.rst @@ -1 +1 @@ -Add support for ``aead_xchacha20_poly1305_rtpsize`` encryption mode for voice connections, and remove deprecated ``xsalsa20_poly1305*`` modes (except ``xsalsa20_poly1305_lite``). +Add support for ``aead_xchacha20_poly1305_rtpsize`` encryption mode for voice connections, and remove deprecated ``xsalsa20_poly1305*`` modes. From deb3ca43442cf0e8942cc3d661f993e23c1c0fc7 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Thu, 15 Aug 2024 13:38:50 +0200 Subject: [PATCH 7/8] deps: require pynacl v1.5.0 --- changelog/1228.misc.rst | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/1228.misc.rst diff --git a/changelog/1228.misc.rst b/changelog/1228.misc.rst new file mode 100644 index 0000000000..505effd2b8 --- /dev/null +++ b/changelog/1228.misc.rst @@ -0,0 +1 @@ +Raise PyNaCl version requirement to ``v1.5.0``. diff --git a/pyproject.toml b/pyproject.toml index 5979d16533..9bab9ae822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", From b8b198efd2585315070391cf1579c064bb3fc3d2 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Thu, 5 Sep 2024 19:41:36 +0200 Subject: [PATCH 8/8] refactor: remove remaining xsalsa20 mode the new `aead_xchacha20_poly1305_rtpsize` is supported by all voice servers already. --- disnake/types/voice.py | 1 - disnake/voice_client.py | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/disnake/types/voice.py b/disnake/types/voice.py index 3377d7ba55..8942db224e 100644 --- a/disnake/types/voice.py +++ b/disnake/types/voice.py @@ -10,7 +10,6 @@ SupportedModes = Literal[ # "aead_aes256_gcm_rtpsize", # supported in libsodium, but not exposed by pynacl "aead_xchacha20_poly1305_rtpsize", - "xsalsa20_poly1305_lite", ] diff --git a/disnake/voice_client.py b/disnake/voice_client.py index 565fcf4448..e9469af670 100644 --- a/disnake/voice_client.py +++ b/disnake/voice_client.py @@ -228,10 +228,7 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: self.ws: DiscordVoiceWebSocket = MISSING warn_nacl = not has_nacl - supported_modes: Tuple[SupportedModes, ...] = ( - "aead_xchacha20_poly1305_rtpsize", - "xsalsa20_poly1305_lite", - ) + supported_modes: Tuple[SupportedModes, ...] = ("aead_xchacha20_poly1305_rtpsize",) @property def guild(self) -> Guild: @@ -541,12 +538,6 @@ def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes + nonce ) - def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: - box = nacl.secret.SecretBox(bytes(self.secret_key)) - nonce, padded_nonce = self._get_nonce(nacl.secret.SecretBox.NONCE_SIZE) - - return header + box.encrypt(bytes(data), padded_nonce).ciphertext + nonce - def play( self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None ) -> None: