Skip to content

Commit

Permalink
Added support for NULL ciphers
Browse files Browse the repository at this point in the history
Added support for NULL ciphers. When they are used, created SRTP and
SRTCP packets are authenticated only (no encryption).

Received SRTP/SRTCP packets are checked if their authentication tag
is valid, and extra SRTP protocol fields are removed before returning
then to application.

Fixed processing of SRTCP packets with E (encryption) bit cleared,
previously duplicate check and tag valiation was not performed, and
whole packet was returned as-is (with extra fields) from decryptRTCP.

Use of NULL ciphers can be enabled independently for SRTP and SRTCP
using SRTPNoEncryption and SRTCPNoEncryption options. They can be used
with key exchange protocols which allows to configure them separately.

Added support for SRTP_NULL_HMAC_SHA1_80 and SRTP_NULL_HMAC_SHA1_32
cipher suites. They use key and salt of the same length as AES_CM_128
ones.

Added new tests to verify test vectors from RFCs.
  • Loading branch information
sirzooro committed Jul 18, 2024
1 parent 3eac369 commit 2efc87e
Show file tree
Hide file tree
Showing 11 changed files with 1,021 additions and 65 deletions.
17 changes: 12 additions & 5 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type Context struct {

sendMKI []byte // Master Key Identifier used for encrypting RTP/RTCP packets. Set to nil if MKI is not enabled.
mkis map[string]srtpCipher // Master Key Identifier to cipher mapping. Used for decrypting packets. Empty if MKI is not enabled.

encryptSRTP bool
encryptSRTCP bool
}

// CreateContext creates a new SRTP Context.
Expand All @@ -83,6 +86,8 @@ func CreateContext(masterKey, masterSalt []byte, profile ProtectionProfile, opts
[]ContextOption{ // Default options
SRTPNoReplayProtection(),
SRTCPNoReplayProtection(),
SRTPEncryption(),
SRTCPEncryption(),
},
opts..., // User specified options
) {
Expand All @@ -91,7 +96,7 @@ func CreateContext(masterKey, masterSalt []byte, profile ProtectionProfile, opts
}
}

c.cipher, err = c.createCipher(c.sendMKI, masterKey, masterSalt)
c.cipher, err = c.createCipher(c.sendMKI, masterKey, masterSalt, c.encryptSRTP, c.encryptSRTCP)
if err != nil {
return nil, err
}
Expand All @@ -116,15 +121,15 @@ func (c *Context) AddCipherForMKI(mki, masterKey, masterSalt []byte) error {
return errMKIAlreadyInUse
}

cipher, err := c.createCipher(mki, masterKey, masterSalt)
cipher, err := c.createCipher(mki, masterKey, masterSalt, c.encryptSRTP, c.encryptSRTCP)
if err != nil {
return err
}
c.mkis[string(mki)] = cipher
return nil
}

func (c *Context) createCipher(mki, masterKey, masterSalt []byte) (srtpCipher, error) {
func (c *Context) createCipher(mki, masterKey, masterSalt []byte, encryptSRTP, encryptSRTCP bool) (srtpCipher, error) {
keyLen, err := c.profile.KeyLen()
if err != nil {
return nil, err
Expand All @@ -143,9 +148,11 @@ func (c *Context) createCipher(mki, masterKey, masterSalt []byte) (srtpCipher, e

switch c.profile {
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return newSrtpCipherAeadAesGcm(c.profile, masterKey, masterSalt, mki)
return newSrtpCipherAeadAesGcm(c.profile, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
return newSrtpCipherAesCmHmacSha1(c.profile, masterKey, masterSalt, mki)
return newSrtpCipherAesCmHmacSha1(c.profile, masterKey, masterSalt, mki, encryptSRTP, encryptSRTCP)
case ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return newSrtpCipherAesCmHmacSha1(c.profile, masterKey, masterSalt, mki, false, false)
default:
return nil, fmt.Errorf("%w: %#v", errNoSuchSRTPProfile, c.profile)
}
Expand Down
34 changes: 34 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,37 @@ func MasterKeyIndicator(mki []byte) ContextOption {
return nil
}
}

// SRTPEncryption enables SRTP encryption.
func SRTPEncryption() ContextOption { // nolint:revive
return func(c *Context) error {
c.encryptSRTP = true
return nil
}
}

// SRTPNoEncryption disables SRTP encryption. This option is useful when you want to use NullCipher for SRTP and keep authentication only.
// It simplifies debugging and testing, but it is not recommended for production use.
func SRTPNoEncryption() ContextOption { // nolint:revive
return func(c *Context) error {
c.encryptSRTP = false
return nil
}
}

// SRTCPEncryption enables SRTCP encryption.
func SRTCPEncryption() ContextOption {
return func(c *Context) error {
c.encryptSRTCP = true
return nil
}
}

// SRTCPNoEncryption disables SRTCP encryption. This option is useful when you want to use NullCipher for SRTCP and keep authentication only.
// It simplifies debugging and testing, but it is not recommended for production use.
func SRTCPNoEncryption() ContextOption {
return func(c *Context) error {
c.encryptSRTCP = false
return nil
}
}
27 changes: 18 additions & 9 deletions protection_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ type ProtectionProfile uint16
// in RFC 5764. They were in earlier draft of this RFC: https://datatracker.ietf.org/doc/html/draft-ietf-avt-dtls-srtp-03#section-4.1.2
// Their IDs are now marked as reserved in the IANA registry. Despite this Chrome supports them:
// https://chromium.googlesource.com/chromium/deps/libsrtp/+/84122798bb16927b1e676bd4f938a6e48e5bf2fe/srtp/include/srtp.h#694
//
// Null profiles disable encryption, they are used for debugging and testing. They are not recommended for production use.
// Use of them is equivalent to using ProtectionProfileAes128CmHmacSha1_NN profile with SRTPNoEncryption and SRTCPNoEncryption options.
const (
ProtectionProfileAes128CmHmacSha1_80 ProtectionProfile = 0x0001
ProtectionProfileAes128CmHmacSha1_32 ProtectionProfile = 0x0002
ProtectionProfileAes256CmHmacSha1_80 ProtectionProfile = 0x0003
ProtectionProfileAes256CmHmacSha1_32 ProtectionProfile = 0x0004
ProtectionProfileNullHmacSha1_80 ProtectionProfile = 0x0005
ProtectionProfileNullHmacSha1_32 ProtectionProfile = 0x0006
ProtectionProfileAeadAes128Gcm ProtectionProfile = 0x0007
ProtectionProfileAeadAes256Gcm ProtectionProfile = 0x0008
)

// KeyLen returns length of encryption key in bytes.
// KeyLen returns length of encryption key in bytes. For all profiles except NullHmacSha1_32 and NullHmacSha1_80 is is also the length of the session key.
func (p ProtectionProfile) KeyLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAeadAes128Gcm:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAeadAes128Gcm, ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return 16, nil
case ProtectionProfileAeadAes256Gcm, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
return 32, nil
Expand All @@ -36,10 +41,10 @@ func (p ProtectionProfile) KeyLen() (int, error) {
}
}

// SaltLen returns length of salt key in bytes.
// SaltLen returns length of salt key in bytes. For all profiles except NullHmacSha1_32 and NullHmacSha1_80 is is also the length of the session salt.
func (p ProtectionProfile) SaltLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80, ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return 14, nil
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return 12, nil
Expand All @@ -51,9 +56,9 @@ func (p ProtectionProfile) SaltLen() (int, error) {
// AuthTagRTPLen returns length of RTP authentication tag in bytes for AES protection profiles. For AEAD ones it returns zero.
func (p ProtectionProfile) AuthTagRTPLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_80:
case ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_80, ProtectionProfileNullHmacSha1_80:
return 10, nil
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_32:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileNullHmacSha1_32:
return 4, nil
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return 0, nil
Expand All @@ -65,7 +70,7 @@ func (p ProtectionProfile) AuthTagRTPLen() (int, error) {
// AuthTagRTCPLen returns length of RTCP authentication tag in bytes for AES protection profiles. For AEAD ones it returns zero.
func (p ProtectionProfile) AuthTagRTCPLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80, ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return 10, nil
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return 0, nil
Expand All @@ -77,7 +82,7 @@ func (p ProtectionProfile) AuthTagRTCPLen() (int, error) {
// AEADAuthTagLen returns length of authentication tag in bytes for AEAD protection profiles. For AES ones it returns zero.
func (p ProtectionProfile) AEADAuthTagLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80, ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return 0, nil
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return 16, nil
Expand All @@ -89,7 +94,7 @@ func (p ProtectionProfile) AEADAuthTagLen() (int, error) {
// AuthKeyLen returns length of authentication key in bytes for AES protection profiles. For AEAD ones it returns zero.
func (p ProtectionProfile) AuthKeyLen() (int, error) {
switch p {
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80:
case ProtectionProfileAes128CmHmacSha1_32, ProtectionProfileAes128CmHmacSha1_80, ProtectionProfileAes256CmHmacSha1_32, ProtectionProfileAes256CmHmacSha1_80, ProtectionProfileNullHmacSha1_32, ProtectionProfileNullHmacSha1_80:
return 20, nil
case ProtectionProfileAeadAes128Gcm, ProtectionProfileAeadAes256Gcm:
return 0, nil
Expand All @@ -113,6 +118,10 @@ func (p ProtectionProfile) String() string {
return "SRTP_AEAD_AES_128_GCM"
case ProtectionProfileAeadAes256Gcm:
return "SRTP_AEAD_AES_256_GCM"
case ProtectionProfileNullHmacSha1_80:
return "SRTP_NULL_HMAC_SHA1_80"
case ProtectionProfileNullHmacSha1_32:
return "SRTP_NULL_HMAC_SHA1_32"
default:
return fmt.Sprintf("Unknown SRTP profile: %#v", p)
}
Expand Down
14 changes: 7 additions & 7 deletions srtcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (

const maxSRTCPIndex = 0x7FFFFFFF

func (c *Context) decryptRTCP(dst, encrypted []byte) ([]byte, error) {
out := allocateIfMismatch(dst, encrypted)
const srtcpHeaderSize = 8

func (c *Context) decryptRTCP(dst, encrypted []byte) ([]byte, error) {
authTagLen, err := c.cipher.AuthTagRTCPLen()
if err != nil {
return nil, err
Expand All @@ -24,12 +24,10 @@ func (c *Context) decryptRTCP(dst, encrypted []byte) ([]byte, error) {
return nil, err
}
mkiLen := len(c.sendMKI)
tailOffset := len(encrypted) - (authTagLen + mkiLen + srtcpIndexSize)

if tailOffset < aeadAuthTagLen {
// Verify that encrypted packet is long enough
if len(encrypted) < (srtcpHeaderSize + aeadAuthTagLen + srtcpIndexSize + mkiLen + authTagLen) {
return nil, fmt.Errorf("%w: %d", errTooShortRTCP, len(encrypted))
} else if isEncrypted := encrypted[tailOffset] >> 7; isEncrypted == 0 {
return out, nil
}

index := c.cipher.getRTCPIndex(encrypted)
Expand All @@ -51,6 +49,8 @@ func (c *Context) decryptRTCP(dst, encrypted []byte) ([]byte, error) {
}
}

out := allocateIfMismatch(dst, encrypted)

out, err = cipher.decryptRTCP(out, encrypted, index, ssrc)
if err != nil {
return nil, err
Expand All @@ -74,7 +74,7 @@ func (c *Context) DecryptRTCP(dst, encrypted []byte, header *rtcp.Header) ([]byt
}

func (c *Context) encryptRTCP(dst, decrypted []byte) ([]byte, error) {
if len(decrypted) < 8 {
if len(decrypted) < srtcpHeaderSize {
return nil, fmt.Errorf("%w: %d", errTooShortRTCP, len(decrypted))
}

Expand Down
12 changes: 10 additions & 2 deletions srtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package srtp

import (
"fmt"

"github.com/pion/rtp"
)

Expand All @@ -13,9 +15,15 @@ func (c *Context) decryptRTP(dst, ciphertext []byte, header *rtp.Header, headerL
if err != nil {
return nil, err
}
aeadAuthTagLen, err := c.cipher.AEADAuthTagLen()
if err != nil {
return nil, err
}
mkiLen := len(c.sendMKI)

if len(ciphertext) < headerLen+len(c.sendMKI)+authTagLen {
return nil, errTooShortRTP
// Verify that encrypted packet is long enough
if len(ciphertext) < (headerLen + aeadAuthTagLen + mkiLen + authTagLen) {
return nil, fmt.Errorf("%w: %d", errTooShortRTP, len(ciphertext))
}

s := c.getSRTPSSRCState(header.SSRC)
Expand Down
82 changes: 66 additions & 16 deletions srtp_cipher_aead_aes_gcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"fmt"

"github.com/pion/rtp"
)
Expand All @@ -23,10 +24,16 @@ type srtpCipherAeadAesGcm struct {
srtpSessionSalt, srtcpSessionSalt []byte

mki []byte

srtpEncrypted, srtcpEncrypted bool
}

func newSrtpCipherAeadAesGcm(profile ProtectionProfile, masterKey, masterSalt, mki []byte) (*srtpCipherAeadAesGcm, error) {
s := &srtpCipherAeadAesGcm{ProtectionProfile: profile}
func newSrtpCipherAeadAesGcm(profile ProtectionProfile, masterKey, masterSalt, mki []byte, encryptSRTP, encryptSRTCP bool) (*srtpCipherAeadAesGcm, error) {
s := &srtpCipherAeadAesGcm{
ProtectionProfile: profile,
srtpEncrypted: encryptSRTP,
srtcpEncrypted: encryptSRTCP,
}

srtpSessionKey, err := aesCmKeyDerivation(labelSRTPEncryption, masterKey, masterSalt, 0, len(masterKey))
if err != nil {
Expand Down Expand Up @@ -87,7 +94,13 @@ func (s *srtpCipherAeadAesGcm) encryptRTP(dst []byte, header *rtp.Header, payloa
}

iv := s.rtpInitializationVector(header, roc)
s.srtpCipher.Seal(dst[n:n], iv[:], payload, dst[:n])
if s.srtpEncrypted {
s.srtpCipher.Seal(dst[n:n], iv[:], payload, dst[:n])
} else {
clearLen := n + len(payload)
copy(dst[n:], payload)
s.srtpCipher.Seal(dst[clearLen:clearLen], iv[:], nil, dst[:clearLen])
}

// Add MKI after the encrypted payload
if len(s.mki) > 0 {
Expand All @@ -113,10 +126,20 @@ func (s *srtpCipherAeadAesGcm) decryptRTP(dst, ciphertext []byte, header *rtp.He
iv := s.rtpInitializationVector(header, roc)

nEnd := len(ciphertext) - len(s.mki)
if _, err := s.srtpCipher.Open(
dst[headerLen:headerLen], iv[:], ciphertext[headerLen:nEnd], ciphertext[:headerLen],
); err != nil {
return nil, err
if s.srtpEncrypted {
if _, err := s.srtpCipher.Open(
dst[headerLen:headerLen], iv[:], ciphertext[headerLen:nEnd], ciphertext[:headerLen],
); err != nil {
return nil, fmt.Errorf("%s: %s", ErrFailedToVerifyAuthTag, err)
}
} else {
nDataEnd := nEnd - authTagLen
if _, err := s.srtpCipher.Open(
nil, iv[:], ciphertext[nDataEnd:nEnd], ciphertext[:nDataEnd],
); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFailedToVerifyAuthTag, err)
}
copy(dst[headerLen:], ciphertext[headerLen:nDataEnd])
}

copy(dst[:headerLen], ciphertext[:headerLen])
Expand All @@ -133,12 +156,25 @@ func (s *srtpCipherAeadAesGcm) encryptRTCP(dst, decrypted []byte, srtcpIndex uin
dst = growBufferSize(dst, aadPos+srtcpIndexSize+len(s.mki))

iv := s.rtcpInitializationVector(srtcpIndex, ssrc)
aad := s.rtcpAdditionalAuthenticatedData(decrypted, srtcpIndex)

s.srtcpCipher.Seal(dst[8:8], iv[:], decrypted[8:], aad[:])
if s.srtcpEncrypted {
aad := s.rtcpAdditionalAuthenticatedData(decrypted, srtcpIndex)
copy(dst[:8], decrypted[:8])
copy(dst[aadPos:aadPos+4], aad[8:12])
s.srtcpCipher.Seal(dst[8:8], iv[:], decrypted[8:], aad[:])
} else {
// Copy the packet unencrypted.
copy(dst, decrypted)
// Append the SRTCP index to the end of the packet - this will form the AAD.
binary.BigEndian.PutUint32(dst[len(decrypted):], srtcpIndex)
// Generate the authentication tag.
tag := make([]byte, authTagLen)
s.srtcpCipher.Seal(tag[0:0], iv[:], nil, dst[:len(decrypted)+4])
// Copy index to the proper place.
copy(dst[aadPos:], dst[len(decrypted):len(decrypted)+4])
// Copy the auth tag after RTCP payload.
copy(dst[len(decrypted):], tag)
}

copy(dst[:8], decrypted[:8])
copy(dst[aadPos:aadPos+4], aad[8:12])
copy(dst[aadPos+4:], s.mki)
return dst, nil
}
Expand All @@ -157,11 +193,25 @@ func (s *srtpCipherAeadAesGcm) decryptRTCP(dst, encrypted []byte, srtcpIndex, ss
}
dst = growBufferSize(dst, nDst)

isEncrypted := encrypted[aadPos]>>7 != 0
iv := s.rtcpInitializationVector(srtcpIndex, ssrc)
aad := s.rtcpAdditionalAuthenticatedData(encrypted, srtcpIndex)

if _, err := s.srtcpCipher.Open(dst[8:8], iv[:], encrypted[8:aadPos], aad[:]); err != nil {
return nil, err
if isEncrypted {
aad := s.rtcpAdditionalAuthenticatedData(encrypted, srtcpIndex)
if _, err := s.srtcpCipher.Open(dst[8:8], iv[:], encrypted[8:aadPos], aad[:]); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFailedToVerifyAuthTag, err)
}
} else {
// Prepare AAD for received packet.
dataEnd := aadPos - authTagLen
aad := make([]byte, dataEnd+4)
copy(aad, encrypted[:dataEnd])
copy(aad[dataEnd:], encrypted[aadPos:aadPos+4])
// Verify the auth tag.
if _, err := s.srtcpCipher.Open(nil, iv[:], encrypted[dataEnd:aadPos], aad); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFailedToVerifyAuthTag, err)
}
// Copy the unencrypted payload.
copy(dst[8:], encrypted[8:dataEnd])
}

copy(dst[:8], encrypted[:8])
Expand Down
Loading

0 comments on commit 2efc87e

Please sign in to comment.