diff --git a/go.mod b/go.mod index 9f5df6e8b..901247971 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.35.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a3c3cc2d1..7b14964d3 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,10 @@ github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxE github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/voice/conn.go b/voice/conn.go index 368352e77..56e55d1b8 100644 --- a/voice/conn.go +++ b/voice/conn.go @@ -210,14 +210,14 @@ func (c *connImpl) handleMessage(op Opcode, data GatewayMessageData) { Data: GatewayMessageDataSelectProtocolData{ Address: ourAddress, Port: ourPort, - Mode: EncryptionModeNormal, + Mode: EncryptionModeAEADXChaCha20Poly1305RTPSize, }, }); err != nil { c.config.Logger.Error("voice: failed to send select protocol", slog.Any("err", err)) } case GatewayMessageDataSessionDescription: - c.udp.SetSecretKey(d.SecretKey) + c.udp.SetSecretKey(d.Mode, d.SecretKey) c.openedChan <- struct{}{} case GatewayMessageDataSpeaking: diff --git a/voice/gateway.go b/voice/gateway.go index ecd879717..cadc92fba 100644 --- a/voice/gateway.go +++ b/voice/gateway.go @@ -27,7 +27,7 @@ var ( ) // GatewayVersion is the version of the voice gateway we are using. -const GatewayVersion = 4 +const GatewayVersion = 8 // Status returns the current status of the gateway. type Status int @@ -109,6 +109,7 @@ type gatewayImpl struct { ssrc uint32 state State + seq int conn *websocket.Conn connMu sync.Mutex @@ -127,13 +128,12 @@ func (g *gatewayImpl) SSRC() uint32 { func (g *gatewayImpl) Open(ctx context.Context, state State) error { g.config.Logger.Debug("opening voice gateway connection") - g.state = state - g.connMu.Lock() defer g.connMu.Unlock() if g.conn != nil { return ErrGatewayAlreadyConnected } + g.state = state g.status = StatusConnecting gatewayURL := fmt.Sprintf("wss://%s?v=%d", state.Endpoint, GatewayVersion) @@ -181,6 +181,7 @@ func (g *gatewayImpl) CloseWithCode(code int, message string) { // clear resume data as we closed gracefully if code == websocket.CloseNormalClosure || code == websocket.CloseGoingAway { g.ssrc = 0 + g.seq = 0 } } } @@ -200,7 +201,10 @@ func (g *gatewayImpl) sendHeartbeat() { ctx, cancel := context.WithTimeout(context.Background(), g.heartbeatInterval) defer cancel() - if err := g.Send(ctx, OpcodeHeartbeat, GatewayMessageDataHeartbeat(g.lastNonce)); err != nil { + if err := g.Send(ctx, OpcodeHeartbeat, GatewayMessageDataHeartbeat{ + T: g.lastNonce, + SeqAck: g.seq, + }); err != nil { if !errors.Is(err, ErrGatewayNotConnected) || errors.Is(err, syscall.EPIPE) { return } @@ -248,6 +252,10 @@ loop: continue } + if message.Seq > 0 { + g.seq = message.Seq + } + switch d := message.D.(type) { case GatewayMessageDataHello: g.status = StatusWaitingForReady @@ -256,7 +264,7 @@ loop: go g.heartbeat() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - if g.ssrc == 0 { + if g.ssrc == 0 || g.seq == 0 { g.status = StatusIdentifying err = g.Send(ctx, OpcodeIdentify, GatewayMessageDataIdentify{ GuildID: g.state.GuildID, @@ -270,6 +278,7 @@ loop: GuildID: g.state.GuildID, SessionID: g.state.SessionID, Token: g.state.Token, + SeqAck: g.seq, }) } cancel() diff --git a/voice/gateway_messages.go b/voice/gateway_messages.go index 1a80c75f0..8c77065da 100644 --- a/voice/gateway_messages.go +++ b/voice/gateway_messages.go @@ -7,8 +7,9 @@ import ( // GatewayMessage represents a voice gateway message type GatewayMessage struct { - Op Opcode `json:"op"` - D GatewayMessageData `json:"d,omitempty"` + Op Opcode `json:"op"` + D GatewayMessageData `json:"d,omitempty"` + Seq int `json:"s,omitempty"` } // UnmarshalJSON unmarshalls the GatewayMessage from json @@ -125,13 +126,16 @@ type GatewayMessageDataHello struct { func (GatewayMessageDataHello) voiceGatewayMessageData() {} -type GatewayMessageDataHeartbeat int64 +type GatewayMessageDataHeartbeat struct { + T int64 `json:"t"` + SeqAck int `json:"seq_ack"` +} func (GatewayMessageDataHeartbeat) voiceGatewayMessageData() {} type GatewayMessageDataSessionDescription struct { - Mode string `json:"mode"` - SecretKey [32]byte `json:"secret_key"` + Mode EncryptionMode `json:"mode"` + SecretKey []byte `json:"secret_key"` } func (GatewayMessageDataSessionDescription) voiceGatewayMessageData() {} @@ -158,11 +162,22 @@ type GatewayMessageDataSelectProtocolData struct { // EncryptionMode is the encryption mode used for voice data. type EncryptionMode string -// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes. +// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#transport-encryption-and-sending-voice. const ( - EncryptionModeNormal EncryptionMode = "xsalsa20_poly1305" - EncryptionModeSuffix EncryptionMode = "xsalsa20_poly1305_suffix" - EncryptionModeLite EncryptionMode = "xsalsa20_poly1305_lite" + // EncryptionModeAEADAES256GCMRTPSize is the preferred encryption mode. + EncryptionModeAEADAES256GCMRTPSize EncryptionMode = "aead_aes256_gcm_rtpsize" + // EncryptionModeAEADXChaCha20Poly1305RTPSize is the required encryption mode. + EncryptionModeAEADXChaCha20Poly1305RTPSize EncryptionMode = "aead_xchacha20_poly1305_rtpsize" + // Deprecated: EncryptionModeXSalsa20Poly1305LiteRTPSize is deprecated. + EncryptionModeXSalsa20Poly1305LiteRTPSize EncryptionMode = "xsalsa20_poly1305_lite_rtpsize" + // Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated. + EncryptionModeAEADAES256GCM EncryptionMode = "aead_aes256_gcm" + // Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated. + EncryptionModeXSalsa20Poly1305 EncryptionMode = "xsalsa20_poly1305" + // Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated. + EncryptionModeXSalsa20Poly1305Suffix EncryptionMode = "xsalsa20_poly1305_suffix" + // Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated. + EncryptionModeXSalsa20Poly1305Lite EncryptionMode = "xsalsa20_poly1305_lite" ) type GatewayMessageDataSpeaking struct { @@ -187,6 +202,7 @@ type GatewayMessageDataResume struct { GuildID snowflake.ID `json:"server_id"` // wtf is this? SessionID string `json:"session_id"` Token string `json:"token"` + SeqAck int `json:"seq"` } func (GatewayMessageDataResume) voiceGatewayMessageData() {} diff --git a/voice/udp_conn.go b/voice/udp_conn.go index a9783cde1..3ef67624e 100644 --- a/voice/udp_conn.go +++ b/voice/udp_conn.go @@ -2,6 +2,7 @@ package voice import ( "context" + "crypto/cipher" "encoding/binary" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "sync" "time" + "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/nacl/secretbox" ) @@ -49,7 +51,7 @@ type ( RemoteAddr() net.Addr // SetSecretKey sets the secret key used to encrypt packets. - SetSecretKey(secretKey [32]byte) + SetSecretKey(encryptionMode EncryptionMode, secretKey []byte) SetDeadline(t time.Time) error @@ -106,12 +108,15 @@ type udpConnImpl struct { conn net.Conn connMu sync.Mutex - packet [12]byte - secretKey [32]byte + cipher cipher.AEAD - sequence uint16 - timestamp uint32 - nonce [24]byte + packet [12]byte + + sequence uint16 + i uint32 + timestamp uint32 + nonce [24]byte + associatedData [12]byte receiveNonce [24]byte receiveBuffer []byte @@ -129,8 +134,23 @@ func (u *udpConnImpl) RemoteAddr() net.Addr { return u.conn.RemoteAddr() } -func (u *udpConnImpl) SetSecretKey(secretKey [32]byte) { - u.secretKey = secretKey +func (u *udpConnImpl) SetSecretKey(encryptionMode EncryptionMode, secretKey []byte) { + var ( + c cipher.AEAD + err error + ) + switch encryptionMode { + case EncryptionModeAEADXChaCha20Poly1305RTPSize: + c, err = chacha20poly1305.NewX(secretKey) + default: + u.config.Logger.Error("unknown encryption mode", slog.String("mode", string(encryptionMode))) + return + } + if err != nil { + u.config.Logger.Error("failed to create cipher", slog.Any("err", err)) + return + } + u.cipher = c } func (u *udpConnImpl) SetDeadline(t time.Time) error { @@ -222,6 +242,9 @@ func (u *udpConnImpl) Write(p []byte) (int, error) { binary.BigEndian.PutUint16(u.packet[2:4], u.sequence) u.sequence++ + u.i++ + binary.BigEndian.PutUint32(u.associatedData[:], u.i) + binary.BigEndian.PutUint32(u.packet[4:8], u.timestamp) u.timestamp += 960 @@ -231,7 +254,9 @@ func (u *udpConnImpl) Write(p []byte) (int, error) { u.connMu.Lock() conn := u.conn u.connMu.Unlock() - if _, err := conn.Write(secretbox.Seal(u.packet[:], p, &u.nonce, &u.secretKey)); err != nil { + + //secretbox.Seal(u.packet[:], p, &u.nonce, &u.secretKey) + if _, err := conn.Write(u.cipher.Seal(u.packet[12:], u.nonce[:], p, u.associatedData[:])); err != nil { return 0, fmt.Errorf("failed to write packet: %w", err) } return len(p), nil @@ -261,7 +286,7 @@ func (u *udpConnImpl) ReadPacket() (*Packet, error) { copy(u.receiveNonce[:], u.receiveBuffer[0:OpusPacketHeaderSize]) - opus, ok := secretbox.Open(nil, u.receiveBuffer[OpusPacketHeaderSize:i], &u.receiveNonce, &u.secretKey) + opus, ok := secretbox.Open(nil, u.receiveBuffer[OpusPacketHeaderSize:i], &u.receiveNonce, nil) if !ok { return nil, ErrDecryptionFailed }