You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
4.3 KiB
166 lines
4.3 KiB
// SPDX-License-Identifier: MIT
|
|
|
|
package clientcore
|
|
|
|
import (
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
// Wire format - SRTP-like mimicry:
|
|
//
|
|
// [12B RTP header | 12B explicit nonce | AEAD ciphertext | 16B tag]
|
|
//
|
|
// RTP header (RFC 3550):
|
|
//
|
|
// byte 0: 0x80 V=2, P=0, X=0, CC=0
|
|
// byte 1: 0x6F M=0, PT=111 (opus, typical voice PT)
|
|
// byte 2-3: seq16 BE monotonic, init random
|
|
// byte 4-7: ts32 BE monotonic, init random, increments by 960 (20ms @ 48kHz)
|
|
// byte 8-11: SSRC random per conn, MSB encodes direction
|
|
//
|
|
// 12B explicit nonce = 4B sessionID || 8B counter (BE). sessionID MSB
|
|
// matches SSRC MSB (direction bit). counter starts at a random uint64.
|
|
// AAD = first 24 bytes (RTP header || nonce).
|
|
//
|
|
// VK TURN appears to forward SRTP-shaped ChannelData on a fast path and
|
|
// drop anomalous payloads. AEAD ciphertext + 16B tag is plausible as
|
|
// AES-GCM SRTP per RFC 7714.
|
|
|
|
const (
|
|
wrapKeyLen = 32
|
|
wrapRTPHdrLen = 12
|
|
wrapNonceLen = 12
|
|
wrapTagLen = 16
|
|
wrapHeaderLen = wrapRTPHdrLen + wrapNonceLen
|
|
wrapOverhead = wrapHeaderLen + wrapTagLen
|
|
wrapRTPVersion = 0x80
|
|
wrapRTPPT = 0x6F
|
|
wrapTSStep = 960
|
|
)
|
|
|
|
type wrapConn struct {
|
|
aead cipher.AEAD
|
|
sessionID [4]byte
|
|
ssrc [4]byte
|
|
counter atomic.Uint64
|
|
seq atomic.Uint32
|
|
timestamp atomic.Uint32
|
|
}
|
|
|
|
func newWrapConn(key []byte, isServer bool) (*wrapConn, error) {
|
|
if len(key) != wrapKeyLen {
|
|
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key))
|
|
}
|
|
aead, err := chacha20poly1305.New(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
|
}
|
|
w := &wrapConn{aead: aead}
|
|
|
|
var rnd [16]byte
|
|
if _, err := rand.Read(rnd[:]); err != nil {
|
|
return nil, fmt.Errorf("wrap: rand init: %w", err)
|
|
}
|
|
copy(w.sessionID[:], rnd[0:4])
|
|
copy(w.ssrc[:], rnd[4:8])
|
|
if isServer {
|
|
w.sessionID[0] |= 0x80
|
|
w.ssrc[0] |= 0x80
|
|
} else {
|
|
w.sessionID[0] &^= 0x80
|
|
w.ssrc[0] &^= 0x80
|
|
}
|
|
w.seq.Store(uint32(binary.BigEndian.Uint16(rnd[8:10])))
|
|
w.timestamp.Store(binary.BigEndian.Uint32(rnd[10:14]))
|
|
|
|
var cb [8]byte
|
|
if _, err := rand.Read(cb[:]); err != nil {
|
|
return nil, fmt.Errorf("wrap: counter rand: %w", err)
|
|
}
|
|
w.counter.Store(binary.BigEndian.Uint64(cb[:]))
|
|
return w, nil
|
|
}
|
|
|
|
func wrapMaxWire(payloadLen int) int {
|
|
return wrapOverhead + payloadLen
|
|
}
|
|
|
|
func (w *wrapConn) wrapInto(dst, payload []byte) (int, error) {
|
|
wireLen := wrapOverhead + len(payload)
|
|
if len(dst) < wireLen {
|
|
return 0, errors.New("wrap: dst buffer too small")
|
|
}
|
|
|
|
dst[0] = wrapRTPVersion
|
|
dst[1] = wrapRTPPT
|
|
seq := uint16(w.seq.Add(1) - 1)
|
|
binary.BigEndian.PutUint16(dst[2:4], seq)
|
|
ts := w.timestamp.Add(wrapTSStep) - wrapTSStep
|
|
binary.BigEndian.PutUint32(dst[4:8], ts)
|
|
copy(dst[8:12], w.ssrc[:])
|
|
|
|
noncePos := wrapRTPHdrLen
|
|
copy(dst[noncePos:noncePos+4], w.sessionID[:])
|
|
ctr := w.counter.Add(1) - 1
|
|
binary.BigEndian.PutUint64(dst[noncePos+4:noncePos+wrapNonceLen], ctr)
|
|
|
|
nonce := dst[noncePos : noncePos+wrapNonceLen]
|
|
aad := dst[:wrapHeaderLen]
|
|
ctPos := wrapHeaderLen
|
|
copy(dst[ctPos:], payload)
|
|
w.aead.Seal(dst[ctPos:ctPos], nonce, dst[ctPos:ctPos+len(payload)], aad)
|
|
|
|
return wireLen, nil
|
|
}
|
|
|
|
func (w *wrapConn) unwrapPacket(wire, dst []byte) (int, error) {
|
|
if len(wire) < wrapOverhead {
|
|
return 0, errors.New("wrap: packet too short")
|
|
}
|
|
nonce := wire[wrapRTPHdrLen : wrapRTPHdrLen+wrapNonceLen]
|
|
aad := wire[:wrapHeaderLen]
|
|
ct := wire[wrapHeaderLen:]
|
|
|
|
plain, err := w.aead.Open(ct[:0], nonce, ct, aad)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("wrap: AEAD open: %w", err)
|
|
}
|
|
if len(plain) > len(dst) {
|
|
return 0, errors.New("wrap: dst buffer too small")
|
|
}
|
|
copy(dst[:len(plain)], plain)
|
|
return len(plain), nil
|
|
}
|
|
|
|
func genWrapKeyHex() (string, error) {
|
|
key := make([]byte, wrapKeyLen)
|
|
if _, err := rand.Read(key); err != nil {
|
|
return "", fmt.Errorf("wrap: key gen: %w", err)
|
|
}
|
|
return hex.EncodeToString(key), nil
|
|
}
|
|
|
|
func decodeWrapKey(enabled bool, raw string) ([]byte, error) {
|
|
if !enabled {
|
|
return nil, nil
|
|
}
|
|
if raw == "" {
|
|
return nil, errors.New("-wrap requires -wrap-key")
|
|
}
|
|
key, err := hex.DecodeString(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("-wrap-key invalid hex: %w", err)
|
|
}
|
|
if len(key) != wrapKeyLen {
|
|
return nil, fmt.Errorf("-wrap-key must decode to %d bytes (got %d)", wrapKeyLen, len(key))
|
|
}
|
|
return key, nil
|
|
}
|
|
|