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

Added support for NULL ciphers #286

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to not have these options, and have everything configured just from Cipher choice?

I am just starting to read the PR, maybe I will understand better as I go :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. MIKEY (RFC 3830) and a=crypto SDP attribute (RFC 4568) allows to disable encryption for SRTP and SRTCP independently. I also would have to create 3 additional crypto profiles for every cipher.

Maybe down the road it would be better to change ProtectionProfile to struct and put various additional params there, and pass it to CreateContext instead of options like it is implemented now.

// 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 @@
if err != nil {
return nil, err
}
aeadAuthTagLen, err := c.cipher.AEADAuthTagLen()
if err != nil {
return nil, err

Check warning on line 20 in srtp.go

View check run for this annotation

Codecov / codecov/patch

srtp.go#L20

Added line #L20 was not covered by tests
}
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 @@
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"fmt"

"github.com/pion/rtp"
)
Expand All @@ -23,10 +24,16 @@
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 @@
}

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 @@
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("%w: %w", ErrFailedToVerifyAuthTag, err)
}
} else {
nDataEnd := nEnd - authTagLen
if _, err := s.srtpCipher.Open(
nil, iv[:], ciphertext[nDataEnd:nEnd], ciphertext[:nDataEnd],
); err != nil {
return nil, fmt.Errorf("%w: %w", ErrFailedToVerifyAuthTag, err)

Check warning on line 140 in srtp_cipher_aead_aes_gcm.go

View check run for this annotation

Codecov / codecov/patch

srtp_cipher_aead_aes_gcm.go#L140

Added line #L140 was not covered by tests
}
copy(dst[headerLen:], ciphertext[headerLen:nDataEnd])
}

copy(dst[:headerLen], ciphertext[:headerLen])
Expand All @@ -133,12 +156,25 @@
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 @@
}
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("%w: %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("%w: %w", ErrFailedToVerifyAuthTag, err)
}
// Copy the unencrypted payload.
copy(dst[8:], encrypted[8:dataEnd])
}

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