182 lines
5.9 KiB
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
|
|
}
|