Skip to content

Commit 3274c76

Browse files
topi314viztea
andauthored
support new voice ecnryption modes (#431)
Co-authored-by: viztea <[email protected]>
1 parent 9fe8944 commit 3274c76

File tree

6 files changed

+306
-61
lines changed

6 files changed

+306
-61
lines changed

_examples/echo/echo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
token = os.Getenv("disgo_token")
2424
guildID = snowflake.GetEnv("disgo_guild_id")
2525
channelID = snowflake.GetEnv("disgo_channel_id")
26+
userID = snowflake.GetEnv("disgo_user_id")
2627
)
2728

2829
func main() {
@@ -76,6 +77,7 @@ func play(client *bot.Client) {
7677
if _, err := conn.UDP().Write(voice.SilenceAudioFrame); err != nil {
7778
panic("error sending silence: " + err.Error())
7879
}
80+
7981
for {
8082
packet, err := conn.UDP().ReadPacket()
8183
if err != nil {
@@ -86,6 +88,9 @@ func play(client *bot.Client) {
8688
slog.Info("error while reading from reader", slog.Any("err", err))
8789
continue
8890
}
91+
if voiceUserID := conn.UserIDBySSRC(packet.SSRC); voiceUserID != userID {
92+
continue
93+
}
8994
if _, err = conn.UDP().Write(packet.Opus); err != nil {
9095
if errors.Is(err, net.ErrClosed) {
9196
slog.Info("connection closed")

voice/audio_sender.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ var SilenceAudioFrame = []byte{0xF8, 0xFF, 0xFE}
1414

1515
const (
1616
// OpusFrameSizeMs is the size of an opus frame in milliseconds.
17-
OpusFrameSizeMs int = 20
17+
OpusFrameSizeMs = 20
1818

1919
// OpusFrameSize is the size of an opus frame in bytes.
20-
OpusFrameSize int = 960
20+
OpusFrameSize = 960
2121

2222
// OpusFrameSizeBytes is the size of an opus frame in bytes.
2323
OpusFrameSizeBytes = OpusFrameSize * 2 * 2

voice/conn.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,28 @@ func (c *connImpl) handleMessage(gateway Gateway, op Opcode, sequenceNumber int,
204204
c.config.Logger.Error("voice: failed to open voiceudp conn", slog.Any("err", err))
205205
break
206206
}
207+
208+
encryptionMode, err := ChooseEncryptionMode(d.Modes)
209+
if err != nil {
210+
c.config.Logger.Error("voice: failed to choose encryption mode", slog.Any("err", err))
211+
break
212+
}
213+
207214
if err = c.Gateway().Send(ctx, OpcodeSelectProtocol, GatewayMessageDataSelectProtocol{
208215
Protocol: ProtocolUDP,
209216
Data: GatewayMessageDataSelectProtocolData{
210217
Address: ourAddress,
211218
Port: ourPort,
212-
Mode: EncryptionModeNormal,
219+
Mode: encryptionMode,
213220
},
214221
}); err != nil {
215222
c.config.Logger.Error("voice: failed to send select protocol", slog.Any("err", err))
216223
}
217224

218225
case GatewayMessageDataSessionDescription:
219-
c.udp.SetSecretKey(d.SecretKey)
226+
if err := c.udp.SetSecretKey(d.Mode, d.SecretKey); err != nil {
227+
c.config.Logger.Error("voice: failed to set secret key", slog.Any("err", err))
228+
}
220229
c.openedChan <- struct{}{}
221230

222231
case GatewayMessageDataSpeaking:

voice/encryption_modes.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package voice
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"encoding/binary"
7+
"fmt"
8+
9+
"golang.org/x/crypto/chacha20poly1305"
10+
)
11+
12+
// NewEncrypter creates a new Encrypter based on the given encryption mode and secret key.
13+
// If the encryption mode is not supported, an error is returned.
14+
func NewEncrypter(encryptionMode EncryptionMode, secretKey []byte) (Encrypter, error) {
15+
switch encryptionMode {
16+
case EncryptionModeNone:
17+
return NewNoopEncrypter(), nil
18+
case EncryptionModeAEADAES256GCMRTPSize, EncryptionModeAEADXChaCha20Poly1305RTPSize:
19+
return NewAEADEncrypter(encryptionMode, secretKey)
20+
21+
default:
22+
return nil, fmt.Errorf("unknown encryption mode: %s", encryptionMode)
23+
}
24+
}
25+
26+
// Encrypter is used to encrypt RTP packets before sending them to Discord.
27+
//
28+
// The header is the 12 byte RTP header.
29+
// The data is the opus encoded audio data.
30+
//
31+
// The returned byte slice is the encrypted packet ready to be sent to Discord.
32+
//
33+
// [NoopEncrypter] does not encrypt the data and is used for testing purposes only.
34+
// [AEADEncrypter] is the required aead_xchacha20_poly1305_rtpsize encryption mode by Discord.
35+
// [AEADAES256GCMRTPSize] is the preferred aead_aes256_gcm_rtpsize encryption mode by Discord.
36+
// See https://discord.com/developers/docs/topics/voice-connections#transport-encryption-and-sending-voice for more information.
37+
type Encrypter interface {
38+
// Encrypt encrypts the given RTP header and opus data and returns the encrypted packet.
39+
Encrypt(header [RTPHeaderSize]byte, data []byte) []byte
40+
41+
// Decrypt decrypts the given packet and returns the RTP header and opus data.
42+
Decrypt(rtpHeaderSize int, packet []byte) ([]byte, error)
43+
}
44+
45+
// NewNoopEncrypter creates a new NoopEncrypter.
46+
func NewNoopEncrypter() *NoopEncrypter {
47+
return &NoopEncrypter{
48+
buf: make([]byte, RTPHeaderSize, MaxOpusFrameSize+RTPHeaderSize),
49+
recBuf: make([]byte, 0, MaxOpusFrameSize+RTPHeaderSize),
50+
}
51+
}
52+
53+
// NoopEncrypter is used to not encrypt RTP packets. This is only for testing purposes.
54+
// Do not use this in production as Discord requires encryption.
55+
type NoopEncrypter struct {
56+
buf []byte
57+
recBuf []byte
58+
}
59+
60+
func (n *NoopEncrypter) Encrypt(header [RTPHeaderSize]byte, data []byte) []byte {
61+
n.buf = n.buf[:RTPHeaderSize]
62+
63+
copy(n.buf, header[:])
64+
n.buf = append(n.buf, data...)
65+
66+
return n.buf
67+
}
68+
69+
func (n *NoopEncrypter) Decrypt(rtpHeaderSize int, packet []byte) ([]byte, error) {
70+
n.recBuf = n.recBuf[:0]
71+
copy(n.recBuf, packet)
72+
73+
return n.recBuf[rtpHeaderSize:], nil
74+
}
75+
76+
// NewAEADEncrypter creates a new AEADEncrypter with the given encryption mode and secret key.
77+
func NewAEADEncrypter(encryptionMode EncryptionMode, secretKey []byte) (*AEADEncrypter, error) {
78+
var aead cipher.AEAD
79+
80+
switch encryptionMode {
81+
case EncryptionModeAEADAES256GCMRTPSize:
82+
block, err := aes.NewCipher(secretKey)
83+
if err != nil {
84+
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
85+
}
86+
87+
c, err := cipher.NewGCM(block)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to create GCM cipher: %w", err)
90+
}
91+
aead = c
92+
case EncryptionModeAEADXChaCha20Poly1305RTPSize:
93+
c, err := chacha20poly1305.NewX(secretKey)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to create XChaCha20-Poly1305 cipher: %w", err)
96+
}
97+
aead = c
98+
default:
99+
return nil, fmt.Errorf("unknown encryption mode: %s", encryptionMode)
100+
}
101+
102+
maxFrameSize := MaxOpusFrameSize + RTPHeaderSize + aead.NonceSize() + aead.Overhead()
103+
104+
return &AEADEncrypter{
105+
cipher: aead,
106+
buf: make([]byte, RTPHeaderSize, maxFrameSize),
107+
nonce: make([]byte, aead.NonceSize()),
108+
seq: 0,
109+
recBuf: make([]byte, 0, maxFrameSize),
110+
recNonce: make([]byte, aead.NonceSize()),
111+
}, nil
112+
}
113+
114+
// AEADEncrypter is used to encrypt RTP packets using AEAD ciphers.
115+
type AEADEncrypter struct {
116+
cipher cipher.AEAD
117+
buf []byte
118+
nonce []byte
119+
seq uint32
120+
121+
recBuf []byte
122+
recNonce []byte
123+
}
124+
125+
func (a *AEADEncrypter) Encrypt(header [RTPHeaderSize]byte, data []byte) []byte {
126+
a.buf = a.buf[:RTPHeaderSize]
127+
128+
binary.LittleEndian.PutUint32(a.nonce, a.seq)
129+
a.seq++
130+
131+
copy(a.buf, header[:])
132+
a.buf = a.cipher.Seal(a.buf, a.nonce, data, header[:])
133+
a.buf = append(a.buf, a.nonce[:4]...)
134+
135+
return a.buf
136+
}
137+
138+
func (a *AEADEncrypter) Decrypt(rtpHeaderSize int, packet []byte) ([]byte, error) {
139+
a.recBuf = a.recBuf[:0]
140+
141+
copy(a.recNonce, packet[len(packet)-4:])
142+
143+
var err error
144+
a.recBuf, err = a.cipher.Open(a.recBuf, a.recNonce, packet[rtpHeaderSize:len(packet)-4], packet[:rtpHeaderSize])
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
return a.recBuf, nil
150+
}

voice/gateway_messages.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ type GatewayMessageDataIdentify struct {
118118
func (GatewayMessageDataIdentify) voiceGatewayMessageData() {}
119119

120120
type GatewayMessageDataReady struct {
121-
SSRC uint32 `json:"ssrc"`
122-
IP string `json:"ip"`
123-
Port int `json:"port"`
124-
Modes []string `json:"modes"`
121+
SSRC uint32 `json:"ssrc"`
122+
IP string `json:"ip"`
123+
Port int `json:"port"`
124+
Modes []EncryptionMode `json:"modes"`
125125
}
126126

127127
func (GatewayMessageDataReady) voiceGatewayMessageData() {}
@@ -148,8 +148,8 @@ type GatewayMessageDataHeartbeat struct {
148148
func (GatewayMessageDataHeartbeat) voiceGatewayMessageData() {}
149149

150150
type GatewayMessageDataSessionDescription struct {
151-
Mode string `json:"mode"`
152-
SecretKey [32]byte `json:"secret_key"`
151+
Mode EncryptionMode `json:"mode"`
152+
SecretKey []byte `json:"secret_key"`
153153
}
154154

155155
func (GatewayMessageDataSessionDescription) voiceGatewayMessageData() {}
@@ -176,13 +176,35 @@ type GatewayMessageDataSelectProtocolData struct {
176176
// EncryptionMode is the encryption mode used for voice data.
177177
type EncryptionMode string
178178

179-
// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes.
179+
// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#transport-encryption-and-sending-voice.
180180
const (
181-
EncryptionModeNormal EncryptionMode = "xsalsa20_poly1305"
182-
EncryptionModeSuffix EncryptionMode = "xsalsa20_poly1305_suffix"
183-
EncryptionModeLite EncryptionMode = "xsalsa20_poly1305_lite"
181+
// EncryptionModeNone is no encryption. This mode is not supported by Discord.
182+
EncryptionModeNone EncryptionMode = ""
183+
// EncryptionModeAEADAES256GCMRTPSize is the preferred encryption mode.
184+
EncryptionModeAEADAES256GCMRTPSize EncryptionMode = "aead_aes256_gcm_rtpsize"
185+
// EncryptionModeAEADXChaCha20Poly1305RTPSize is the required encryption mode.
186+
EncryptionModeAEADXChaCha20Poly1305RTPSize EncryptionMode = "aead_xchacha20_poly1305_rtpsize"
184187
)
185188

189+
// AllEncryptionModes is a list of all supported EncryptionMode(s).
190+
var AllEncryptionModes = []EncryptionMode{
191+
EncryptionModeAEADAES256GCMRTPSize, // preferred
192+
EncryptionModeAEADXChaCha20Poly1305RTPSize, // required
193+
}
194+
195+
// ChooseEncryptionMode chooses the best supported encryption mode from the given list of modes.
196+
// It returns an error if no supported mode is found.
197+
func ChooseEncryptionMode(modes []EncryptionMode) (EncryptionMode, error) {
198+
for _, preferred := range AllEncryptionModes {
199+
for _, mode := range modes {
200+
if mode == preferred {
201+
return mode, nil
202+
}
203+
}
204+
}
205+
return "", fmt.Errorf("no supported encryption mode found in %v", modes)
206+
}
207+
186208
type GatewayMessageDataSpeaking struct {
187209
Speaking SpeakingFlags `json:"speaking"`
188210
Delay int `json:"delay"`

0 commit comments

Comments
 (0)