Files
VerseVDI-Data-Plane/gateway/apollo_media.go

182 lines
5.9 KiB
Go

package gateway
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
)
const (
// Apollo limits clear audio payloads to 1400 bytes and uses AES-CBC with
// PKCS#7 padding. FEC adds its fixed 12-byte header outside that ciphertext.
// This is the largest accepted UDP datagram, not an allocation hint.
apolloMediaMaximumPacket = 12 + 12 + 1408
apolloVideoHeaderSize = 32
apolloRTPHeaderSize = 12
apolloVideoNVHeaderSize = 16
apolloVideoRawPacketSize = 1024 + 16
)
var (
errApolloMedia = errors.New("apollo media malformed")
errApolloMediaParity = errors.New("apollo media parity packet")
)
type apolloMediaCodec struct {
block cipher.Block
aead cipher.AEAD
keyID uint32
}
type apolloRTPPacket struct {
extension bool
payloadType byte
sequence uint16
payload []byte
}
type apolloAudioShard struct {
sequence uint16
timestamp uint32
ssrc uint32
base uint16
parityIndex uint8
parity bool
payload []byte
}
func newApolloMediaCodec(key []byte, keyID uint32) (*apolloMediaCodec, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &apolloMediaCodec{block: block, aead: aead, keyID: keyID}, nil
}
func apolloMediaPing(payload []byte, sequence uint32) []byte {
if len(payload) != 16 {
return nil
}
ping := make([]byte, 20)
copy(ping, payload)
binary.BigEndian.PutUint32(ping[16:], sequence)
return ping
}
func (c *apolloMediaCodec) OpenVideo(packet []byte) (apolloVideoShard, error) {
if c == nil || c.aead == nil || len(packet) != apolloVideoHeaderSize+apolloVideoRawPacketSize {
return apolloVideoShard{}, errApolloMedia
}
sealed := make([]byte, len(packet)-apolloVideoHeaderSize+apolloControlTagSize)
copy(sealed, packet[apolloVideoHeaderSize:])
copy(sealed[len(packet)-apolloVideoHeaderSize:], packet[16:apolloVideoHeaderSize])
plaintext, err := c.aead.Open(nil, packet[:12], sealed, nil)
if err != nil {
return apolloVideoShard{}, errApolloMedia
}
rtp, err := parseApolloRTP(plaintext)
if err != nil || !rtp.extension || rtp.payloadType != 0 || len(rtp.payload) != 1024 {
return apolloVideoShard{}, errApolloMedia
}
nv := rtp.payload[:apolloVideoNVHeaderSize]
flags := nv[8]
fecInfo := binary.LittleEndian.Uint32(nv[12:16])
dataPackets := int(fecInfo >> 22)
fecIndex := int((fecInfo >> 12) & 0x03ff)
fecPercent := int((fecInfo >> 4) & 0xff)
if dataPackets < 1 || dataPackets > apolloVideoMaximumDataShards {
return apolloVideoShard{}, errApolloMedia
}
parityPackets := (dataPackets*fecPercent + 99) / 100
if dataPackets+parityPackets > 255 || fecIndex >= dataPackets+parityPackets {
return apolloVideoShard{}, errApolloMedia
}
block := (nv[11] >> 4) & 0x03
lastBlock := (nv[11] >> 6) & 0x03
if block > lastBlock {
return apolloVideoShard{}, errApolloMedia
}
return apolloVideoShard{
frame: binary.LittleEndian.Uint32(nv[4:8]),
block: block,
lastBlock: lastBlock,
dataPackets: dataPackets,
parity: parityPackets,
index: fecIndex,
sequence: rtp.sequence,
streamIndex: binary.LittleEndian.Uint32(nv[:4]) >> 8,
flags: flags,
payload: append([]byte(nil), rtp.payload[apolloVideoNVHeaderSize:]...),
}, nil
}
func (c *apolloMediaCodec) OpenAudio(packet []byte) (apolloAudioShard, error) {
if c == nil || c.block == nil || len(packet) <= apolloRTPHeaderSize || len(packet) > apolloMediaMaximumPacket {
return apolloAudioShard{}, errApolloMedia
}
rtp, err := parseApolloRTP(packet)
if err != nil || rtp.extension || len(rtp.payload) == 0 {
return apolloAudioShard{}, errApolloMedia
}
if rtp.payloadType == 97 && len(rtp.payload)%aes.BlockSize == 0 {
return apolloAudioShard{
sequence: rtp.sequence, timestamp: binary.BigEndian.Uint32(packet[4:8]), ssrc: binary.BigEndian.Uint32(packet[8:12]),
base: rtp.sequence &^ 3, payload: append([]byte(nil), rtp.payload...),
}, nil
}
if rtp.payloadType != 127 || len(rtp.payload) <= 12 || len(rtp.payload)-12 > 1408 || rtp.payload[0] > 1 || rtp.payload[1] != 97 {
return apolloAudioShard{}, errApolloMedia
}
base := binary.BigEndian.Uint16(rtp.payload[2:4])
if base&3 != 0 || len(rtp.payload[12:])%aes.BlockSize != 0 {
return apolloAudioShard{}, errApolloMedia
}
return apolloAudioShard{
sequence: rtp.sequence, timestamp: binary.BigEndian.Uint32(rtp.payload[4:8]), ssrc: binary.BigEndian.Uint32(rtp.payload[8:12]),
base: base, parityIndex: rtp.payload[0], parity: true, payload: append([]byte(nil), rtp.payload[12:]...),
}, nil
}
func (c *apolloMediaCodec) openApolloAudioCipher(sequence uint16, payload []byte) ([]byte, error) {
if c == nil || c.block == nil || len(payload) == 0 || len(payload) > 1408 || len(payload)%aes.BlockSize != 0 {
return nil, errApolloMedia
}
plaintext := append([]byte(nil), payload...)
iv := make([]byte, aes.BlockSize)
binary.BigEndian.PutUint32(iv, c.keyID+uint32(sequence))
cipher.NewCBCDecrypter(c.block, iv).CryptBlocks(plaintext, plaintext)
padding := int(plaintext[len(plaintext)-1])
if padding == 0 || padding > aes.BlockSize || padding > len(plaintext) {
return nil, errApolloMedia
}
for _, value := range plaintext[len(plaintext)-padding:] {
if int(value) != padding {
return nil, errApolloMedia
}
}
return append([]byte(nil), plaintext[:len(plaintext)-padding]...), nil
}
func parseApolloRTP(packet []byte) (apolloRTPPacket, error) {
if len(packet) < apolloRTPHeaderSize || packet[0]>>6 != 2 || packet[0]&0x2f != 0 {
return apolloRTPPacket{}, errApolloMedia
}
offset := apolloRTPHeaderSize
extension := packet[0]&0x10 != 0
if extension {
if len(packet) < offset+4 || binary.BigEndian.Uint16(packet[14:16]) != 0 {
return apolloRTPPacket{}, errApolloMedia
}
offset += 4
}
if offset >= len(packet) {
return apolloRTPPacket{}, errApolloMedia
}
return apolloRTPPacket{extension: extension, payloadType: packet[1] & 0x7f, sequence: binary.BigEndian.Uint16(packet[2:4]), payload: packet[offset:]}, nil
}