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

Support cryptex. #1969

Merged
merged 11 commits into from
Feb 17, 2023
2 changes: 1 addition & 1 deletion jitsi-media-transform/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>jitsi-srtp</artifactId>
<version>1.1-7-gd8d1435</version>
<version>1.1-12-ga64adcc</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class EventTimeline(
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
open class PacketInfo @JvmOverloads constructor(
var packet: Packet,
/** The original length of the packet, i.e. before decryption. Stays unchanged even if the packet is updated. */
val originalLength: Int = packet.length,
val timeline: EventTimeline = EventTimeline()
) {
/**
Expand All @@ -96,6 +98,9 @@ open class PacketInfo @JvmOverloads constructor(
}
}

/** Whether the packet originally had cryptex RTP header extensions. */
var originalHadCryptex: Boolean = false

/**
* Whether this packet has been recognized to contain only shouldDiscard.
*/
Expand Down Expand Up @@ -141,13 +146,14 @@ open class PacketInfo @JvmOverloads constructor(
*/
fun clone(): PacketInfo {
val clone = if (ENABLE_TIMELINE) {
PacketInfo(packet.clone(), timeline.clone())
PacketInfo(packet.clone(), originalLength, timeline.clone())
} else {
// If the timeline isn't enabled, we can just share the same one.
// (This would change if we allowed enabling the timeline at runtime)
PacketInfo(packet.clone(), timeline)
PacketInfo(packet.clone(), originalLength, timeline)
}
clone.receivedTime = receivedTime
clone.originalHadCryptex = originalHadCryptex
clone.shouldDiscard = shouldDiscard
clone.endpointId = endpointId
clone.layeringChanged = layeringChanged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,20 @@ class RtpReceiverImpl @JvmOverloads constructor(
path = pipeline {
node(PacketLossNode(packetLossConfig), condition = { packetLossConfig.enabled })
node(RtpParser(streamInformationStore, logger))
node(tccGenerator)
node(remoteBandwidthEstimator)
// TODO: temporarily putting the audioLevelReader node here such that we can determine whether
// or not a packet should be discarded before doing SRTP. audioLevelReader has been moved here
// (instead of introducing a different class to read audio levels) to avoid parsing the RTP
// header extensions twice (which is expensive). In the future we will parse and cache the
// header extensions to make this lookup more efficient, at which time we could move
// audioLevelReader back to where it was (in the audio path) and add a new node here which would
// check for different discard conditions (i.e. checking the audio level for silence)
node(audioLevelReader)
node(audioLevelReader.preDecryptNode)
node(videoMuteNode)
node(srtpDecryptWrapper)
node(tccGenerator)
node(remoteBandwidthEstimator)
// This reads audio levels from packets that use cryptex. TODO: should it go in the Audio path?
node(audioLevelReader.postDecryptNode)
node(toggleablePcapWriter.newObserverNode())
node(statsTracker)
node(PaddingTermination(logger))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,18 +269,25 @@ class Transceiver(
streamInformationStore.addSsrcAssociation(ssrcAssociation)
}

fun setSrtpInformation(chosenSrtpProtectionProfile: Int, tlsRole: TlsRole, keyingMaterial: ByteArray) {
fun setSrtpInformation(
chosenSrtpProtectionProfile: Int,
tlsRole: TlsRole,
keyingMaterial: ByteArray,
cryptex: Boolean
) {
val srtpProfileInfo =
SrtpUtil.getSrtpProfileInformationFromSrtpProtectionProfile(chosenSrtpProtectionProfile)
logger.cdebug {
"Transceiver $id creating transformers with:\n" +
"profile info:\n$srtpProfileInfo\n" +
"tls role: $tlsRole"
"tls role: $tlsRole\n" +
"cryptex: $cryptex"
}
srtpTransformers = SrtpUtil.initializeTransformer(
srtpProfileInfo,
keyingMaterial,
tlsRole,
cryptex,
logger
).also { setSrtpInformationInternal(it, true) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class SrtpUtil {
srtpProfileInformation: SrtpProfileInformation,
keyingMaterial: ByteArray,
tlsRole: TlsRole,
cryptex: Boolean,
parentLogger: Logger
): SrtpTransformers {
val clientWriteSrtpMasterKey = ByteArray(srtpProfileInformation.cipherKeyLength)
Expand Down Expand Up @@ -167,6 +168,8 @@ class SrtpUtil {
/* TODO: disable this only in cases where we actually need to use retransmitPlain? */
srtpPolicy.isSendReplayEnabled = false

srtpPolicy.isCryptexEnabled = cryptex

val clientSrtpContextFactory = SrtpContextFactory(
tlsRole == TlsRole.CLIENT,
clientWriteSrtpMasterKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ class RtpParser(
return null
}

packetInfo.packet = when (payloadType.mediaType) {
val rtpPacket = when (payloadType.mediaType) {
MediaType.AUDIO -> when (payloadType.encoding) {
RED -> packet.toOtherType(::RedAudioRtpPacket)
else -> packet.toOtherType(::AudioRtpPacket)
}
MediaType.VIDEO -> packet.toOtherType(::VideoRtpPacket)
else -> throw Exception("Unrecognized media type: '${payloadType.mediaType}'")
}
packetInfo.packet = rtpPacket
if (rtpPacket.extensionsProfileType == 0xC0DE || rtpPacket.extensionsProfileType == 0xC2DE) {
packetInfo.originalHadCryptex = true
}

packetInfo.resetPayloadVerification()
return packetInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ import org.jitsi.rtp.rtp.header_extensions.AudioLevelHeaderExtension
*/
class AudioLevelReader(
streamInformationStore: ReadOnlyStreamInformationStore
) : ObserverNode("Audio level reader") {
) {
/**
* Process packets without cryptex pre-SRTP to allow the "skip decryption" optimization if they are to be dropped.
*/
val preDecryptNode = AudioLevelReaderNode("Audio level reader (pre-srtp)") { !it.originalHadCryptex }
val postDecryptNode = AudioLevelReaderNode("Audio level reader (post-srtp)") { it.originalHadCryptex }

private var audioLevelExtId: Int? = null
var audioLevelListener: AudioLevelListener? = null
var forwardedSilencePackets: Int = 0
Expand All @@ -49,48 +55,56 @@ class AudioLevelReader(
}
}

override fun observe(packetInfo: PacketInfo) {
val audioRtpPacket = packetInfo.packet as? AudioRtpPacket ?: return

audioLevelExtId?.let { audioLevelId ->
audioRtpPacket.getHeaderExtension(audioLevelId)?.let { ext ->
stats.audioLevel()

val level = AudioLevelHeaderExtension.getAudioLevel(ext)
val silence = level == MUTED_LEVEL

if (!silence) stats.nonSilence(AudioLevelHeaderExtension.getVad(ext))
if (silence && forwardedSilencePackets > forwardedSilencePacketsLimit) {
packetInfo.shouldDiscard = true
stats.discardedSilence()
} else if (this.forceMute) {
packetInfo.shouldDiscard = true
stats.discardedForceMute()
} else {
forwardedSilencePackets = if (silence) forwardedSilencePackets + 1 else 0
audioLevelListener?.let { listener ->
if (listener.onLevelReceived(audioRtpPacket.ssrc, (127 - level).toPositiveLong())) {
packetInfo.shouldDiscard = true
stats.discardedRanking()
inner class AudioLevelReaderNode(
name: String,
val shouldProcess: (PacketInfo) -> Boolean
) : ObserverNode(name) {

override fun observe(packetInfo: PacketInfo) {
if (!shouldProcess(packetInfo)) return

val audioRtpPacket = packetInfo.packet as? AudioRtpPacket ?: return

audioLevelExtId?.let { audioLevelId ->
audioRtpPacket.getHeaderExtension(audioLevelId)?.let { ext ->
stats.audioLevel()

val level = AudioLevelHeaderExtension.getAudioLevel(ext)
val silence = level == MUTED_LEVEL

if (!silence) stats.nonSilence(AudioLevelHeaderExtension.getVad(ext))
if (silence && forwardedSilencePackets > forwardedSilencePacketsLimit) {
packetInfo.shouldDiscard = true
stats.discardedSilence()
} else if ([email protected]) {
packetInfo.shouldDiscard = true
stats.discardedForceMute()
} else {
forwardedSilencePackets = if (silence) forwardedSilencePackets + 1 else 0
audioLevelListener?.let { listener ->
if (listener.onLevelReceived(audioRtpPacket.ssrc, (127 - level).toPositiveLong())) {
packetInfo.shouldDiscard = true
stats.discardedRanking()
}
}
}
}
}
}
}

override fun getNodeStats(): NodeStatsBlock = super.getNodeStats().apply {
addString("audio_level_ext_id", audioLevelExtId.toString())
addNumber("num_audio_levels", stats.numAudioLevels)
addNumber("num_silence_packets_discarded", stats.numDiscardedSilence)
addNumber("num_force_mute_discarded", stats.numDiscardedForceMute)
addNumber("num_ranking_discarded", stats.numDiscardedRanking)
addNumber("num_non_silence", stats.numNonSilence)
addNumber("num_non_silence_with_vad", stats.numNonSilenceWithVad)
addBoolean("force_mute", forceMute)
}
override fun getNodeStats(): NodeStatsBlock = super.getNodeStats().apply {
addString("audio_level_ext_id", audioLevelExtId.toString())
addNumber("num_audio_levels", stats.numAudioLevels)
addNumber("num_silence_packets_discarded", stats.numDiscardedSilence)
addNumber("num_force_mute_discarded", stats.numDiscardedForceMute)
addNumber("num_ranking_discarded", stats.numDiscardedRanking)
addNumber("num_non_silence", stats.numNonSilence)
addNumber("num_non_silence_with_vad", stats.numNonSilenceWithVad)
addBoolean("force_mute", forceMute)
}

override fun trace(f: () -> Unit) = f.invoke()
override fun trace(f: () -> Unit) = f.invoke()
}

companion object {
const val MUTED_LEVEL = 127
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class RemoteBandwidthEstimator(
AbsSendTimeHeaderExtension.getTime(ext),
packetInfo.receivedTime,
rtpPacket.sequenceNumber,
rtpPacket.length.bytes
packetInfo.originalLength.bytes
)
/* With receiver-side bwe we need to treat each received packet as separate feedback */
bwe.feedbackComplete(now)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SrtpTransformerFactory {
srtpData.srtpProfileInformation,
srtpData.keyingMaterial,
srtpData.tlsRole,
cryptex = false, /* TODO: add tests for the cryptex=true case */
StdoutLogger()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal class SrtpDecryptTest : ShouldSpec() {
SrtpSample.srtpProfileInformation,
SrtpSample.keyingMaterial.array(),
SrtpSample.tlsRole,
cryptex = false, /* TODO: add tests for cryptex case */
StdoutLogger()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal class SrtpEncryptTest : ShouldSpec() {
SrtpSample.srtpProfileInformation,
SrtpSample.keyingMaterial.array(),
SrtpSample.tlsRole,
cryptex = false, /* TODO: add tests for cryptex case */
StdoutLogger()
)

Expand Down
31 changes: 31 additions & 0 deletions jvb/src/main/kotlin/org/jitsi/videobridge/CryptexConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.videobridge

import org.jitsi.config.JitsiConfig
import org.jitsi.metaconfig.config
import org.jitsi.metaconfig.from

class CryptexConfig private constructor() {
companion object {
val endpoint: Boolean by config(
"videobridge.cryptex.endpoint".from(JitsiConfig.newConfig)
)
val relay: Boolean by config(
"videobridge.cryptex.relay".from(JitsiConfig.newConfig)
)
}
}
9 changes: 7 additions & 2 deletions jvb/src/main/kotlin/org/jitsi/videobridge/Endpoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ class Endpoint @JvmOverloads constructor(

/* TODO: do we ever want to support useUniquePort for an Endpoint? */
private val iceTransport = IceTransport(id, iceControlling, false, logger)
private val dtlsTransport = DtlsTransport(logger)
private val dtlsTransport = DtlsTransport(logger).also { it.cryptex = CryptexConfig.endpoint }

private var cryptex: Boolean = CryptexConfig.endpoint

private val diagnosticContext = conference.newDiagnosticContext().apply {
put("endpoint_id", id)
Expand Down Expand Up @@ -417,7 +419,7 @@ class Endpoint @JvmOverloads constructor(
keyingMaterial: ByteArray
) {
logger.info("DTLS handshake complete")
transceiver.setSrtpInformation(chosenSrtpProtectionProfile, tlsRole, keyingMaterial)
transceiver.setSrtpInformation(chosenSrtpProtectionProfile, tlsRole, keyingMaterial, cryptex)
// TODO(brian): the old code would work even if the sctp connection was created after
// the handshake had completed, but this won't (since this is a one-time event). do
// we need to worry about that case?
Expand Down Expand Up @@ -771,6 +773,9 @@ class Endpoint @JvmOverloads constructor(
} else {
logger.info("Ignoring empty DtlsFingerprint extension: ${transportInfo.toXML()}")
}
if (CryptexConfig.endpoint) {
cryptex = cryptex && fingerprintExtension.cryptex
}
}
dtlsTransport.setRemoteFingerprints(remoteFingerprints)
if (fingerprintExtensions.isNotEmpty()) {
Expand Down
10 changes: 9 additions & 1 deletion jvb/src/main/kotlin/org/jitsi/videobridge/relay/Relay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import org.jitsi.utils.logging2.createChildLogger
import org.jitsi.utils.queue.CountingErrorHandler
import org.jitsi.videobridge.AbstractEndpoint
import org.jitsi.videobridge.Conference
import org.jitsi.videobridge.CryptexConfig
import org.jitsi.videobridge.EncodingsManager
import org.jitsi.videobridge.Endpoint
import org.jitsi.videobridge.PotentialPacketHandler
Expand Down Expand Up @@ -149,7 +150,9 @@ class Relay @JvmOverloads constructor(
private var expired = false

private val iceTransport = IceTransport(id, iceControlling, useUniquePort, logger, clock)
private val dtlsTransport = DtlsTransport(logger)
private val dtlsTransport = DtlsTransport(logger).also { it.cryptex = CryptexConfig.relay }

private var cryptex = CryptexConfig.relay

private val diagnosticContext = conference.newDiagnosticContext().apply {
put("relay_id", id)
Expand Down Expand Up @@ -362,6 +365,7 @@ class Relay @JvmOverloads constructor(
srtpProfileInfo,
keyingMaterial,
tlsRole,
cryptex,
logger
)
this.srtpTransformers = srtpTransformers
Expand Down Expand Up @@ -389,6 +393,10 @@ class Relay @JvmOverloads constructor(
} else {
logger.info("Ignoring empty DtlsFingerprint extension: ${transportInfo.toXML()}")
}

if (CryptexConfig.relay) {
cryptex = cryptex && fingerprintExtension.cryptex
}
}
dtlsTransport.setRemoteFingerprints(remoteFingerprints)
if (fingerprintExtensions.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class DtlsTransport(parentLogger: Logger) {

private val stats = Stats()

/** Whether to advertise cryptex to peers. */
var cryptex = false

/**
* The DTLS stack instance
*/
Expand Down Expand Up @@ -159,6 +162,9 @@ class DtlsTransport(parentLogger: Logger) {
}
fingerprintPE.fingerprint = dtlsStack.localFingerprint
fingerprintPE.hash = dtlsStack.localFingerprintHashFunction
if (cryptex) {
fingerprintPE.cryptex = true
}
}

/**
Expand Down
Loading