diff --git a/voice/src/main/kotlin/VoiceConnection.kt b/voice/src/main/kotlin/VoiceConnection.kt index 4a4f8576ead9..5ccf6bd21138 100644 --- a/voice/src/main/kotlin/VoiceConnection.kt +++ b/voice/src/main/kotlin/VoiceConnection.kt @@ -4,6 +4,7 @@ import dev.kord.common.annotation.KordVoice import dev.kord.common.entity.Snowflake import dev.kord.gateway.Gateway import dev.kord.gateway.UpdateVoiceStatus +import dev.kord.voice.encryption.strategies.NonceStrategy import dev.kord.voice.gateway.VoiceGateway import dev.kord.voice.gateway.VoiceGatewayConfiguration import dev.kord.voice.handlers.StreamsHandler @@ -40,6 +41,8 @@ data class VoiceConnectionData( * @param data the data representing this [VoiceConnection]. * @param voiceGatewayConfiguration the configuration used on each new [connect] for the [voiceGateway]. * @param audioProvider a [AudioProvider] that will provide [AudioFrame] when required. + * @param frameSender the [AudioFrameSender] that will handle the sending of audio packets. + * @param nonceStrategy the [NonceStrategy] that is used during encryption of audio. * @param frameInterceptorFactory a factory for [FrameInterceptor]s that is used whenever audio is ready to be sent. See [FrameInterceptor] and [DefaultFrameInterceptor]. */ @KordVoice @@ -52,6 +55,7 @@ class VoiceConnection( val streams: Streams, val audioProvider: AudioProvider, val frameSender: AudioFrameSender, + val nonceStrategy: NonceStrategy, val frameInterceptorFactory: (FrameInterceptorContext) -> FrameInterceptor, ) { val scope: CoroutineScope = diff --git a/voice/src/main/kotlin/VoiceConnectionBuilder.kt b/voice/src/main/kotlin/VoiceConnectionBuilder.kt index bfbf64908fff..53eff201a940 100644 --- a/voice/src/main/kotlin/VoiceConnectionBuilder.kt +++ b/voice/src/main/kotlin/VoiceConnectionBuilder.kt @@ -8,6 +8,8 @@ import dev.kord.gateway.Gateway import dev.kord.gateway.UpdateVoiceStatus import dev.kord.gateway.VoiceServerUpdate import dev.kord.gateway.VoiceStateUpdate +import dev.kord.voice.encryption.strategies.LiteNonceStrategy +import dev.kord.voice.encryption.strategies.NonceStrategy import dev.kord.voice.exception.VoiceConnectionInitializationException import dev.kord.voice.gateway.DefaultVoiceGatewayBuilder import dev.kord.voice.gateway.VoiceGateway @@ -46,6 +48,12 @@ class VoiceConnectionBuilder( */ var audioSender: AudioFrameSender? = null + /** + * The nonce strategy to be used for the encryption of audio packets. + * If `null`, [dev.kord.voice.encryption.strategies.LiteNonceStrategy] will be used. + */ + var nonceStrategy: NonceStrategy? = null + fun audioProvider(provider: AudioProvider) { this.audioProvider = provider } @@ -154,9 +162,10 @@ class VoiceConnectionBuilder( val audioProvider = audioProvider ?: EmptyAudioPlayerProvider val audioSender = audioSender ?: DefaultAudioFrameSender(DefaultAudioFrameSenderData(udpSocket)) + val nonceStrategy = nonceStrategy ?: LiteNonceStrategy() val frameInterceptorFactory = frameInterceptorFactory ?: { DefaultFrameInterceptor(it) } val streams = - streams ?: if (receiveVoice) DefaultStreams(voiceGateway, udpSocket) else NOPStreams + streams ?: if (receiveVoice) DefaultStreams(voiceGateway, udpSocket, nonceStrategy) else NOPStreams return VoiceConnection( voiceConnectionData, @@ -167,6 +176,7 @@ class VoiceConnectionBuilder( streams, audioProvider, audioSender, + nonceStrategy, frameInterceptorFactory, ) } diff --git a/voice/src/main/kotlin/encryption/XSalsa20Poly1305Encryption.kt b/voice/src/main/kotlin/encryption/XSalsa20Poly1305Encryption.kt index b5f6148e04c9..7203c9916f6f 100644 --- a/voice/src/main/kotlin/encryption/XSalsa20Poly1305Encryption.kt +++ b/voice/src/main/kotlin/encryption/XSalsa20Poly1305Encryption.kt @@ -23,9 +23,6 @@ internal class XSalsa20Poly1305Encryption(private val key: ByteArray) { output.writeByteArray(c, boxzerobytesLength, messageBufferLength - boxzerobytesLength) - // TODO: implement a nonce strategy - output.writeByteArray(nonce, length = 4) - return true } diff --git a/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt new file mode 100644 index 000000000000..b47ad36408a4 --- /dev/null +++ b/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt @@ -0,0 +1,36 @@ +package dev.kord.voice.encryption.strategies + +import dev.kord.voice.io.ByteArrayView +import dev.kord.voice.io.MutableByteArrayCursor +import dev.kord.voice.io.mutableCursor +import dev.kord.voice.io.view +import dev.kord.voice.udp.RTPPacket +import kotlinx.atomicfu.atomic + +class LiteNonceStrategy : NonceStrategy { + override val nonceLength: Int = 4 + + private var count: Int by atomic(0) + private val nonceBuffer: ByteArray = ByteArray(4) + private val nonceView = nonceBuffer.view() + private val nonceCursor = nonceBuffer.mutableCursor() + + override fun strip(packet: RTPPacket): ByteArrayView { + return with(packet.payload) { + val nonce = view(dataEnd - 4, dataEnd)!! + resize(dataStart, dataEnd - 4) + nonce + } + } + + override fun generate(header: () -> ByteArrayView): ByteArrayView { + count++ + nonceCursor.reset() + nonceCursor.writeInt(count) + return nonceView + } + + override fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) { + cursor.writeByteView(nonce) + } +} diff --git a/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt new file mode 100644 index 000000000000..158352580f4e --- /dev/null +++ b/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt @@ -0,0 +1,30 @@ +package dev.kord.voice.encryption.strategies + +import dev.kord.voice.io.ByteArrayView +import dev.kord.voice.io.MutableByteArrayCursor +import dev.kord.voice.udp.RTPPacket + +/** + * An [encryption mode, regarding the nonce](https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes), supported by Discord. + */ +sealed interface NonceStrategy { + /** + * The amount of bytes this nonce will take up. + */ + val nonceLength: Int + + /** + * Reads the nonce from this [packet] (also removes it if it resides in the payload), and returns a [ByteArrayView] of it. + */ + fun strip(packet: RTPPacket): ByteArrayView + + /** + * Generates a nonce, may use the provided information. + */ + fun generate(header: () -> ByteArrayView): ByteArrayView + + /** + * Writes the [nonce] to [cursor] in the correct relative position. + */ + fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) +} \ No newline at end of file diff --git a/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt new file mode 100644 index 000000000000..5d29e6bdc372 --- /dev/null +++ b/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt @@ -0,0 +1,31 @@ +package dev.kord.voice.encryption.strategies + +import dev.kord.voice.io.ByteArrayView +import dev.kord.voice.io.MutableByteArrayCursor +import dev.kord.voice.io.mutableCursor +import dev.kord.voice.io.view +import dev.kord.voice.udp.RTPPacket +import dev.kord.voice.udp.RTP_HEADER_LENGTH + +class NormalNonceStrategy : NonceStrategy { + // the nonce is already a part of the rtp header, which means this will take up no extra space. + override val nonceLength: Int = 0 + + private val rtpHeaderBuffer = ByteArray(RTP_HEADER_LENGTH) + private val rtpHeaderCursor = rtpHeaderBuffer.mutableCursor() + private val rtpHeaderView = rtpHeaderBuffer.view() + + override fun strip(packet: RTPPacket): ByteArrayView { + rtpHeaderCursor.reset() + packet.writeHeader(rtpHeaderCursor) + return rtpHeaderView + } + + override fun generate(header: () -> ByteArrayView): ByteArrayView { + return header() + } + + override fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) { + /* the nonce is the rtp header which means this should do nothing */ + } +} \ No newline at end of file diff --git a/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt new file mode 100644 index 000000000000..1514a3641e5a --- /dev/null +++ b/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt @@ -0,0 +1,33 @@ +package dev.kord.voice.encryption.strategies + +import dev.kord.voice.io.ByteArrayView +import dev.kord.voice.io.MutableByteArrayCursor +import dev.kord.voice.io.view +import dev.kord.voice.udp.RTPPacket +import kotlin.random.Random + +private const val SUFFIX_NONCE_LENGTH = 24 + +class SuffixNonceStrategy : NonceStrategy { + override val nonceLength: Int = SUFFIX_NONCE_LENGTH + + private val nonceBuffer: ByteArray = ByteArray(SUFFIX_NONCE_LENGTH) + private val nonceView = nonceBuffer.view() + + override fun strip(packet: RTPPacket): ByteArrayView { + return with(packet.payload) { + val nonce = view(dataEnd - SUFFIX_NONCE_LENGTH, dataEnd)!! + resize(dataStart, dataEnd - SUFFIX_NONCE_LENGTH) + nonce + } + } + + override fun generate(header: () -> ByteArrayView): ByteArrayView { + Random.Default.nextBytes(nonceBuffer) + return nonceView + } + + override fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) { + cursor.writeByteView(nonce) + } +} \ No newline at end of file diff --git a/voice/src/main/kotlin/handlers/UdpLifeCycleHandler.kt b/voice/src/main/kotlin/handlers/UdpLifeCycleHandler.kt index fe2facaeea13..5dd0bd105ebd 100644 --- a/voice/src/main/kotlin/handlers/UdpLifeCycleHandler.kt +++ b/voice/src/main/kotlin/handlers/UdpLifeCycleHandler.kt @@ -6,6 +6,9 @@ import dev.kord.common.annotation.KordVoice import dev.kord.voice.EncryptionMode import dev.kord.voice.FrameInterceptorContextBuilder import dev.kord.voice.VoiceConnection +import dev.kord.voice.encryption.strategies.LiteNonceStrategy +import dev.kord.voice.encryption.strategies.NormalNonceStrategy +import dev.kord.voice.encryption.strategies.SuffixNonceStrategy import dev.kord.voice.gateway.* import dev.kord.voice.udp.AudioFrameSenderConfiguration import io.ktor.util.network.* @@ -38,12 +41,18 @@ internal class UdpLifeCycleHandler( udpLifeCycleLogger.trace { "ip discovered for voice successfully" } + val encryptionMode = when (connection.nonceStrategy) { + is LiteNonceStrategy -> EncryptionMode.XSalsa20Poly1305Lite + is NormalNonceStrategy -> EncryptionMode.XSalsa20Poly1305 + is SuffixNonceStrategy -> EncryptionMode.XSalsa20Poly1305Suffix + } + val selectProtocol = SelectProtocol( protocol = "udp", data = SelectProtocol.Data( address = ip.hostname, port = ip.port, - mode = EncryptionMode.XSalsa20Poly1305Lite + mode = encryptionMode ) ) @@ -55,6 +64,7 @@ internal class UdpLifeCycleHandler( val config = AudioFrameSenderConfiguration( ssrc = ssrc!!, key = it.secretKey.toUByteArray().toByteArray(), + nonceStrategy = nonceStrategy, provider = audioProvider, baseFrameInterceptorContext = FrameInterceptorContextBuilder(gateway, voiceGateway), interceptorFactory = frameInterceptorFactory, diff --git a/voice/src/main/kotlin/streams/DefaultStreams.kt b/voice/src/main/kotlin/streams/DefaultStreams.kt index 9caf66d4216d..f30c91c12b91 100644 --- a/voice/src/main/kotlin/streams/DefaultStreams.kt +++ b/voice/src/main/kotlin/streams/DefaultStreams.kt @@ -5,6 +5,7 @@ import dev.kord.common.annotation.KordVoice import dev.kord.common.entity.Snowflake import dev.kord.voice.AudioFrame import dev.kord.voice.encryption.XSalsa20Poly1305Codec +import dev.kord.voice.encryption.strategies.NonceStrategy import dev.kord.voice.gateway.Speaking import dev.kord.voice.gateway.VoiceGateway import dev.kord.voice.io.* @@ -27,13 +28,14 @@ private val defaultStreamsLogger = KotlinLogging.logger { } class DefaultStreams( private val voiceGateway: VoiceGateway, private val udp: VoiceUdpSocket, + private val nonceStrategy: NonceStrategy ) : Streams { private fun CoroutineScope.listenForIncoming(key: ByteArray, server: NetworkAddress) { udp.incoming .filter { it.address == server } .mapNotNull { RTPPacket.fromPacket(it.packet) } .filter { it.payloadType == PayloadType.Audio.raw } - .decrypt(key) + .decrypt(nonceStrategy, key) .clean() .onEach { _incomingAudioPackets.emit(it) } .launchIn(this) @@ -84,7 +86,7 @@ class DefaultStreams( override val ssrcToUser: Map by _ssrcToUser } -private fun Flow.decrypt(key: ByteArray): Flow { +private fun Flow.decrypt(nonceStrategy: NonceStrategy, key: ByteArray): Flow { val codec = XSalsa20Poly1305Codec(key) val nonceBuffer = ByteArray(TweetNaclFast.SecretBox.nonceLength).mutableCursor() @@ -94,10 +96,13 @@ private fun Flow.decrypt(key: ByteArray): Flow { return mapNotNull { nonceBuffer.reset() - nonceBuffer.writeByteArray(it.payload.data, it.payload.dataEnd - 4, 4) - decryptedCursor.reset() - val decrypted = with(it.payload) { codec.decrypt(data, dataStart, viewSize - 4, nonceBuffer.data, decryptedCursor) } + + nonceBuffer.writeByteView(nonceStrategy.strip(it)) + + val decrypted = with(it.payload) { + codec.decrypt(data, dataStart, viewSize, nonceBuffer.data, decryptedCursor) + } if (!decrypted) { defaultStreamsLogger.trace { "failed to decrypt the packet with data ${it.payload.data.contentToString()} at offset ${it.payload.dataStart} and length ${it.payload.viewSize - 4}" } diff --git a/voice/src/main/kotlin/udp/AudioFrameSender.kt b/voice/src/main/kotlin/udp/AudioFrameSender.kt index 4d367bec47bc..7ca290593f68 100644 --- a/voice/src/main/kotlin/udp/AudioFrameSender.kt +++ b/voice/src/main/kotlin/udp/AudioFrameSender.kt @@ -7,6 +7,7 @@ import dev.kord.voice.AudioProvider import dev.kord.voice.FrameInterceptor import dev.kord.voice.FrameInterceptorContext import dev.kord.voice.FrameInterceptorContextBuilder +import dev.kord.voice.encryption.strategies.NonceStrategy import io.ktor.util.network.* @KordVoice @@ -14,6 +15,7 @@ data class AudioFrameSenderConfiguration( val server: NetworkAddress, val ssrc: UInt, val key: ByteArray, + val nonceStrategy: NonceStrategy, val provider: AudioProvider, val baseFrameInterceptorContext: FrameInterceptorContextBuilder, val interceptorFactory: (FrameInterceptorContext) -> FrameInterceptor diff --git a/voice/src/main/kotlin/udp/AudioPacketProvider.kt b/voice/src/main/kotlin/udp/AudioPacketProvider.kt index 5bcb645ab56f..209d30f52d59 100644 --- a/voice/src/main/kotlin/udp/AudioPacketProvider.kt +++ b/voice/src/main/kotlin/udp/AudioPacketProvider.kt @@ -2,40 +2,32 @@ package dev.kord.voice.udp import com.iwebpp.crypto.TweetNaclFast import dev.kord.voice.encryption.XSalsa20Poly1305Codec +import dev.kord.voice.encryption.strategies.NonceStrategy import dev.kord.voice.io.ByteArrayView import dev.kord.voice.io.MutableByteArrayCursor import dev.kord.voice.io.mutableCursor import dev.kord.voice.io.view -abstract class AudioPacketProvider(val key: ByteArray) { +abstract class AudioPacketProvider(val key: ByteArray, val nonceStrategy: NonceStrategy) { abstract fun provide(sequence: UShort, timestamp: UInt, ssrc: UInt, data: ByteArray): ByteArrayView } private class CouldNotEncryptDataException(val data: ByteArray) : RuntimeException("Couldn't encrypt the following data: [${data.joinToString(", ")}]") -class DefaultAudioPackerProvider(key: ByteArray) : AudioPacketProvider(key) { +class DefaultAudioPackerProvider(key: ByteArray, nonceStrategy: NonceStrategy) : + AudioPacketProvider(key, nonceStrategy) { private val codec = XSalsa20Poly1305Codec(key) - private var nonce: Int = 0 - private val packetBuffer = ByteArray(2048) private val packetBufferCursor: MutableByteArrayCursor = packetBuffer.mutableCursor() private val packetBufferView: ByteArrayView = packetBuffer.view() - private val nonceBuffer: MutableByteArrayCursor = ByteArray(TweetNaclFast.SecretBox.nonceLength).mutableCursor() - - private val lock: Any = Any() - private fun loadNonce() { - // reset cursor position to 0 - nonceBuffer.reset() + private val rtpHeaderView: ByteArrayView = packetBuffer.view(0, RTP_HEADER_LENGTH)!! - // write the 4 byte nonce - nonceBuffer.writeInt(nonce) + private val nonceBuffer: MutableByteArrayCursor = ByteArray(TweetNaclFast.SecretBox.nonceLength).mutableCursor() - // increment it for next call - nonce++ - } + private val lock: Any = Any() private fun MutableByteArrayCursor.writeHeader(sequence: Short, timestamp: Int, ssrc: Int) { writeByte(((2 shl 6) or (0x0) or (0x0)).toByte()) // first 2 bytes are version. the rest @@ -48,27 +40,27 @@ class DefaultAudioPackerProvider(key: ByteArray) : AudioPacketProvider(key) { override fun provide(sequence: UShort, timestamp: UInt, ssrc: UInt, data: ByteArray): ByteArrayView = synchronized(lock) { with(packetBufferCursor) { - reset() - loadNonce() + this.reset() + nonceBuffer.reset() - // encrypt data - val encryptedStart = cursor - val encrypted = codec.encrypt(data, nonce = nonceBuffer.data, output = this) - val encryptedLength = cursor - encryptedStart + // make sure we enough room in this buffer + resize(RTP_HEADER_LENGTH + (data.size + TweetNaclFast.SecretBox.boxzerobytesLength) + nonceStrategy.nonceLength) - if (!encrypted) throw CouldNotEncryptDataException(data) + // write header and generate nonce + writeHeader(sequence.toShort(), timestamp.toInt(), ssrc.toInt()) - // let's keep track of where the actual packet starts in the buffer - val initial = cursor + val rawNonce = nonceStrategy.generate { rtpHeaderView } + nonceBuffer.writeByteView(rawNonce) - // make sure we enough room in this buffer - resize(cursor + RTP_HEADER_LENGTH + encryptedLength) + // encrypt data and write into our buffer + val encrypted = codec.encrypt(data, nonce = nonceBuffer.data, output = this) - writeHeader(sequence.toShort(), timestamp.toInt(), ssrc.toInt()) - writeByteArray(this.data, encryptedStart, encryptedLength) + if (!encrypted) throw CouldNotEncryptDataException(data) + + nonceStrategy.append(rawNonce, this) // let's make sure we have the correct view of the packet - if (!packetBufferView.resize(initial, cursor)) error("couldn't resize packet buffer view") + if (!packetBufferView.resize(0, cursor)) error("couldn't resize packet buffer view?!") packetBufferView } diff --git a/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt b/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt index 974e3aa047c5..f4a53cc7caa6 100644 --- a/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt +++ b/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt @@ -33,7 +33,7 @@ class DefaultAudioFrameSender( val interceptor: FrameInterceptor = createFrameInterceptor(configuration) var sequence: UShort = Random.nextBits(UShort.SIZE_BITS).toUShort() - val packetProvider = DefaultAudioPackerProvider(configuration.key) + val packetProvider = DefaultAudioPackerProvider(configuration.key, configuration.nonceStrategy) val frames = Channel(Channel.RENDEZVOUS) with(configuration.provider) { launch { provideFrames(frames) } }