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 17, 2024
1 parent 2a52aa0 commit d9f095c
Show file tree
Hide file tree
Showing 11 changed files with 1,008 additions and 65 deletions.
37 changes: 32 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 @@ -102,6 +107,26 @@ func CreateContext(masterKey, masterSalt []byte, profile ProtectionProfile, opts
return c, nil
}

// createContextWithCipher creates a new SRTP Context with a pre-created cipher. This is used for testing purposes only.
func createContextWithCipher(profile ProtectionProfile, cipher srtpCipher) (*Context, error) {
c := &Context{
srtpSSRCStates: map[uint32]*srtpSSRCState{},
srtcpSSRCStates: map[uint32]*srtcpSSRCState{},
profile: profile,
mkis: map[string]srtpCipher{},
cipher: cipher,
}
err := SRTPNoReplayProtection()(c)
if err != nil {
return nil, err
}
err = SRTCPNoReplayProtection()(c)
if err != nil {
return nil, err
}
return c, nil
}

// AddCipherForMKI adds new MKI with associated masker key and salt. Context must be created with MasterKeyIndicator option
// to enable MKI support. MKI must be unique and have the same length as the one used for creating Context.
// Operation is not thread-safe, you need to provide synchronization with decrypting packets.
Expand All @@ -116,15 +141,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 +168,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 {
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 {
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
10 changes: 10 additions & 0 deletions srtp_cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,13 @@ It is *before* the ESRTCP word (Encrypted-flag and SRTCP index).
See https://tools.ietf.org/html/rfc7714 for the full specifications.
*/

// deriveSessionKeys should be used in tests only. RFCs test vectors specifes derived keys to use, this struct is used to inject them into the cipher in tests.
type derivedSessionKeys struct {
srtpSessionKey []byte
srtpSessionSalt []byte
srtpSessionAuthTag []byte
srtcpSessionKey []byte
srtcpSessionSalt []byte
srtcpSessionAuthTag []byte
}
Loading

0 comments on commit d9f095c

Please sign in to comment.