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

Implement AES Encryption for Voice #897

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
126 changes: 109 additions & 17 deletions voice/api/voice.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public sealed class EncryptionMode(
public constructor(`value`: String) : this(value, null)
}

public object AeadAes256Gcm : EncryptionMode("aead_aes256_gcm")

public object AeadAes256GcmRtpSize : EncryptionMode("aead_aes256_gcm_rtpsize")

public object XSalsa20Poly1305 : EncryptionMode("xsalsa20_poly1305")

public object XSalsa20Poly1305Suffix : EncryptionMode("xsalsa20_poly1305_suffix")
Expand All @@ -75,6 +79,8 @@ public sealed class EncryptionMode(
*/
public val entries: List<EncryptionMode> by lazy(mode = PUBLICATION) {
listOf(
AeadAes256Gcm,
AeadAes256GcmRtpSize,
XSalsa20Poly1305,
XSalsa20Poly1305Suffix,
XSalsa20Poly1305Lite,
Expand All @@ -87,6 +93,8 @@ public sealed class EncryptionMode(
* specified [value].
*/
public fun from(`value`: String): EncryptionMode = when (value) {
"aead_aes256_gcm" -> AeadAes256Gcm
"aead_aes256_gcm_rtpsize" -> AeadAes256GcmRtpSize
"xsalsa20_poly1305" -> XSalsa20Poly1305
"xsalsa20_poly1305_suffix" -> XSalsa20Poly1305Suffix
"xsalsa20_poly1305_lite" -> XSalsa20Poly1305Lite
Expand Down
4 changes: 3 additions & 1 deletion voice/src/main/kotlin/EncryptionMode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
STRING_KORD_ENUM, name = "EncryptionMode",
docUrl = "https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes",
entries = [
Entry("AeadAes256Gcm", stringValue = "aead_aes256_gcm"),
Entry("AeadAes256GcmRtpSize", stringValue = "aead_aes256_gcm_rtpsize"),
Entry("XSalsa20Poly1305", stringValue = "xsalsa20_poly1305"),
Entry("XSalsa20Poly1305Suffix", stringValue = "xsalsa20_poly1305_suffix"),
Entry("XSalsa20Poly1305Lite", stringValue = "xsalsa20_poly1305_lite")
Entry("XSalsa20Poly1305Lite", stringValue = "xsalsa20_poly1305_lite"),
]
)

Expand Down
3 changes: 2 additions & 1 deletion voice/src/main/kotlin/VoiceConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.VoiceEncryption
import dev.kord.voice.encryption.strategies.NonceStrategy
import dev.kord.voice.gateway.VoiceGateway
import dev.kord.voice.gateway.VoiceGatewayConfiguration
Expand Down Expand Up @@ -54,7 +55,7 @@ public class VoiceConnection(
public val audioProvider: AudioProvider,
public val frameInterceptor: FrameInterceptor,
public val frameSender: AudioFrameSender,
public val nonceStrategy: NonceStrategy,
public val encryption: VoiceEncryption,
connectionDetachDuration: Duration
) {
public val scope: CoroutineScope =
Expand Down
21 changes: 13 additions & 8 deletions voice/src/main/kotlin/VoiceConnectionBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,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.VoiceEncryption
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
Expand All @@ -31,7 +31,7 @@ public class VoiceConnectionBuilder(
public var gateway: Gateway,
public var selfId: Snowflake,
public var channelId: Snowflake,
public var guildId: Snowflake
public var guildId: Snowflake,
) {
/**
* The amount in milliseconds to wait for the events required to create a [VoiceConnection]. Default is 5000, or 5 seconds.
Expand Down Expand Up @@ -65,9 +65,10 @@ public class VoiceConnectionBuilder(

/**
* The nonce strategy to be used for the encryption of audio packets.
* If `null`, [dev.kord.voice.encryption.strategies.LiteNonceStrategy] will be used.
* If `null` & voice receive if disabled, [VoiceEncryption.AeadAes256Gcm] will be used,
* otherwise [VoiceEncryption.XSalsaPoly1305] with the Lite strategy will be used.
*/
public var nonceStrategy: NonceStrategy? = null
public var encryption: VoiceEncryption? = null

/**
* A boolean indicating whether your voice state will be muted.
Expand Down Expand Up @@ -166,19 +167,23 @@ public class VoiceConnectionBuilder(
.build()
val udpSocket = udpSocket ?: GlobalVoiceUdpSocket
val audioProvider = audioProvider ?: EmptyAudioPlayerProvider
val nonceStrategy = nonceStrategy ?: LiteNonceStrategy()
val encryption = if ((receiveVoice || streams != null) && encryption?.supportsDecryption != true) {
VoiceEncryption.XSalsaPoly1305()
} else {
encryption ?: VoiceEncryption.AeadAes256Gcm
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to log if the user's chosen encryption method ends up not being used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even throw an exception? but logging a warning is probably best.

val frameInterceptor = frameInterceptor ?: DefaultFrameInterceptor()
val audioSender =
audioSender ?: DefaultAudioFrameSender(
DefaultAudioFrameSenderData(
udpSocket,
frameInterceptor,
audioProvider,
nonceStrategy
encryption
)
)
val streams =
streams ?: if (receiveVoice) DefaultStreams(voiceGateway, udpSocket, nonceStrategy) else NOPStreams
streams ?: if (receiveVoice) DefaultStreams(voiceGateway, udpSocket, encryption) else NOPStreams

return VoiceConnection(
voiceConnectionData,
Expand All @@ -190,7 +195,7 @@ public class VoiceConnectionBuilder(
audioProvider,
frameInterceptor,
audioSender,
nonceStrategy,
encryption,
connectionDetachDuration
)
}
Expand Down
163 changes: 163 additions & 0 deletions voice/src/main/kotlin/encryption/VoiceEncryption.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package dev.kord.voice.encryption

import com.iwebpp.crypto.TweetNaclFast
import dev.kord.voice.EncryptionMode
import dev.kord.voice.encryption.strategies.LiteNonceStrategy
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
import dev.kord.voice.udp.RTPPacket
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

public sealed interface VoiceEncryption {
public val mode: EncryptionMode

public val nonceLength: Int

public val supportsDecryption: Boolean get() = true
viztea marked this conversation as resolved.
Show resolved Hide resolved

public fun createBox(key: ByteArray): Box

public fun createUnbox(key: ByteArray): Unbox

@JvmInline
public value class XSalsaPoly1305(public val nsf: NonceStrategy.Factory = LiteNonceStrategy) : VoiceEncryption {
viztea marked this conversation as resolved.
Show resolved Hide resolved
viztea marked this conversation as resolved.
Show resolved Hide resolved
override val mode: EncryptionMode
get() = nsf.mode

override val nonceLength: Int
get() = 24

override fun createBox(key: ByteArray): Box = object : Box {
private val codec: XSalsa20Poly1305Codec = XSalsa20Poly1305Codec(key)
private val nonceStrategy: NonceStrategy = nsf.create()

override val overhead: Int
get() = TweetNaclFast.SecretBox.boxzerobytesLength + nsf.nonceLength

override fun encrypt(src: ByteArray, nonce: ByteArray, dst: MutableByteArrayCursor): Boolean {
return codec.encrypt(src, 0, src.size, nonce, dst)
}

override fun generateNonce(header: () -> ByteArrayView): ByteArrayView {
return nonceStrategy.generate(header)
}

override fun appendNonce(nonce: ByteArrayView, dst: MutableByteArrayCursor) {
nonceStrategy.append(nonce, dst)
}
}

override fun createUnbox(key: ByteArray): Unbox = object : Unbox {
private val codec: XSalsa20Poly1305Codec = XSalsa20Poly1305Codec(key)
private val nonceStrategy: NonceStrategy = nsf.create()

override fun decrypt(
src: ByteArray,
srcOff: Int,
srcLen: Int,
nonce: ByteArray,
dst: MutableByteArrayCursor,
): Boolean = codec.decrypt(src, srcOff, srcLen, nonce, dst)

override fun getNonce(packet: RTPPacket): ByteArrayView = nonceStrategy.strip(packet)
}
}

public data object AeadAes256Gcm : VoiceEncryption {
private const val AUTH_TAG_LEN = 16
private const val NONCE_LEN = 4
private const val IV_LEN = 12

override val mode: EncryptionMode
get() = EncryptionMode.AeadAes256Gcm

override val nonceLength: Int
get() = 4

override val supportsDecryption: Boolean
get() = false

override fun createBox(key: ByteArray): Box = object : Box {
//
viztea marked this conversation as resolved.
Show resolved Hide resolved
private val iv = ByteArray(IV_LEN)
private val ivCursor = iv.mutableCursor()

//
private var nonce = 0u
private val nonceBuffer: ByteArray = ByteArray(NONCE_LEN)
private val nonceCursor = nonceBuffer.mutableCursor()
private val nonceView = nonceBuffer.view()

//
val secretKey = SecretKeySpec(key, "AES")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")

override val overhead: Int
get() = AUTH_TAG_LEN + NONCE_LEN

override fun encrypt(src: ByteArray, nonce: ByteArray, dst: MutableByteArrayCursor): Boolean {
iv.fill(0)
ivCursor.reset()
ivCursor.writeByteArray(nonce)

cipher.init(
Cipher.ENCRYPT_MODE, secretKey,
GCMParameterSpec(AUTH_TAG_LEN * 8, iv, 0, IV_LEN)
)
cipher.updateAAD(dst.data.copyOfRange(0, dst.cursor))
dst.cursor += cipher.doFinal(src, 0, src.size, dst.data, dst.cursor)

return true
}

override fun appendNonce(nonce: ByteArrayView, dst: MutableByteArrayCursor) {
dst.writeByteView(nonce)
}

override fun generateNonce(header: () -> ByteArrayView): ByteArrayView {
nonceCursor.reset()
nonceCursor.writeInt(nonce++.toInt())
return nonceView
}
}

override fun createUnbox(key: ByteArray): Unbox {
throw UnsupportedOperationException()
}
}

public interface Box {
public val overhead: Int

public fun encrypt(src: ByteArray, nonce: ByteArray, dst: MutableByteArrayCursor): Boolean

public fun generateNonce(header: () -> ByteArrayView): ByteArrayView

public fun appendNonce(nonce: ByteArrayView, dst: MutableByteArrayCursor)
}

public interface Unbox {
/**
* Decrypt the packet.
*/
public fun decrypt(
src: ByteArray,
srcOff: Int,
srcLen: Int,
nonce: ByteArray,
dst: MutableByteArrayCursor,
): Boolean

/**
* Strip the nonce from the [RTP packet][packet].
*
* @return the nonce.
*/
public fun getNonce(packet: RTPPacket): ByteArrayView
}
}
11 changes: 5 additions & 6 deletions voice/src/main/kotlin/encryption/XSalsa20Poly1305Codec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class XSalsa20Poly1305Codec(public val key: ByteArray) {
mOffset: Int = 0,
mLength: Int = message.size,
nonce: ByteArray,
output: MutableByteArrayCursor
output: MutableByteArrayCursor,
): Boolean =
encryption.box(message, mOffset, mLength, nonce, output)

Expand All @@ -21,16 +21,15 @@ public class XSalsa20Poly1305Codec(public val key: ByteArray) {
boxOffset: Int = 0,
boxLength: Int = box.size,
nonce: ByteArray,
output: MutableByteArrayCursor
): Boolean =
encryption.open(box, boxOffset, boxLength, nonce, output)
output: MutableByteArrayCursor,
): Boolean = encryption.open(box, boxOffset, boxLength, nonce, output)
}

public fun XSalsa20Poly1305Codec.encrypt(
message: ByteArray,
mOffset: Int = 0,
mLength: Int = message.size,
nonce: ByteArray
nonce: ByteArray,
): ByteArray? {
val buffer = ByteArray(mLength + TweetNaclFast.SecretBox.boxzerobytesLength)
if (!encrypt(message, mOffset, mLength, nonce, buffer.mutableCursor())) return null
Expand All @@ -41,7 +40,7 @@ public fun XSalsa20Poly1305Codec.decrypt(
box: ByteArray,
boxOffset: Int = 0,
boxLength: Int = box.size,
nonce: ByteArray
nonce: ByteArray,
): ByteArray? {
val buffer = ByteArray(boxLength - TweetNaclFast.SecretBox.boxzerobytesLength)
if (!decrypt(box, boxOffset, boxLength, nonce, buffer.mutableCursor())) return null
Expand Down
10 changes: 8 additions & 2 deletions voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.kord.voice.encryption.strategies

import dev.kord.voice.EncryptionMode
import dev.kord.voice.io.ByteArrayView
import dev.kord.voice.io.MutableByteArrayCursor
import dev.kord.voice.io.mutableCursor
Expand All @@ -8,8 +9,6 @@ import dev.kord.voice.udp.RTPPacket
import kotlinx.atomicfu.atomic

public 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()
Expand All @@ -33,4 +32,11 @@ public class LiteNonceStrategy : NonceStrategy {
override fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) {
cursor.writeByteView(nonce)
}

public companion object Factory : NonceStrategy.Factory {
override val mode: EncryptionMode get() = EncryptionMode.XSalsa20Poly1305Lite
override val nonceLength: Int get() = 4

override fun create(): NonceStrategy = LiteNonceStrategy()
}
}
19 changes: 14 additions & 5 deletions voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.kord.voice.encryption.strategies

import dev.kord.voice.EncryptionMode
import dev.kord.voice.io.ByteArrayView
import dev.kord.voice.io.MutableByteArrayCursor
import dev.kord.voice.udp.RTPPacket
Expand All @@ -8,11 +9,6 @@ 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.
*/
public sealed interface NonceStrategy {
/**
* The amount of bytes this nonce will take up.
*/
public val nonceLength: Int

/**
* Reads the nonce from this [packet] (also removes it if it resides in the payload), and returns a [ByteArrayView] of it.
*/
Expand All @@ -27,4 +23,17 @@ public sealed interface NonceStrategy {
* Writes the [nonce] to [cursor] in the correct relative position.
*/
public fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor)

public interface Factory {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this factory? i'd rather not add it if not needed.

/**
*/
public val mode: EncryptionMode

/**
* The amount of bytes this nonce will take up.
*/
public val nonceLength: Int

public fun create(): NonceStrategy
}
}
Loading