This commit is contained in:
seydx
2026-01-17 20:22:08 +01:00
parent c03cd9f156
commit 160695857e
18 changed files with 613 additions and 2314 deletions
+35
View File
@@ -0,0 +1,35 @@
package tutk
import (
"crypto/sha256"
"encoding/base64"
"strings"
)
func CalculateAuthKey(enr, mac string) []byte {
data := enr + strings.ToUpper(mac)
hash := sha256.Sum256([]byte(data))
b64 := base64.StdEncoding.EncodeToString(hash[:6])
b64 = strings.ReplaceAll(b64, "+", "Z")
b64 = strings.ReplaceAll(b64, "/", "9")
b64 = strings.ReplaceAll(b64, "=", "A")
return []byte(b64)
}
func DerivePSK(enr string) []byte {
// DerivePSK derives the DTLS PSK from ENR
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
// contains a 0x00 byte, the PSK is truncated at that position.
hash := sha256.Sum256([]byte(enr))
pskLen := 32
for i := range 32 {
if hash[i] == 0x00 {
pskLen = i
break
}
}
psk := make([]byte, 32)
copy(psk[:pskLen], hash[:pskLen])
return psk
}
@@ -72,7 +72,7 @@ func computeNonce(iv []byte, epoch uint16, sequenceNumber uint64) []byte {
binary.BigEndian.PutUint64(nonce[4:], sequenceNumber)
binary.BigEndian.PutUint16(nonce[4:], epoch)
for i := 0; i < chachaNonceLength; i++ {
for i := range chachaNonceLength {
nonce[i] ^= iv[i]
}
+50
View File
@@ -0,0 +1,50 @@
package tutk
// https://github.com/seydx/tutk_wyze#11-codec-reference
const (
CodecMPEG4 byte = 0x4C
CodecH263 byte = 0x4D
CodecH264 byte = 0x4E
CodecMJPEG byte = 0x4F
CodecH265 byte = 0x50
)
const (
CodecAACRaw byte = 0x86
CodecAACADTS byte = 0x87
CodecAACLATM byte = 0x88
CodecPCMU byte = 0x89
CodecPCMA byte = 0x8A
CodecADPCM byte = 0x8B
CodecPCML byte = 0x8C
CodecSPEEX byte = 0x8D
CodecMP3 byte = 0x8E
CodecG726 byte = 0x8F
CodecAACAlt byte = 0x90
CodecOpus byte = 0x92
)
var sampleRates = [9]uint32{8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000}
func GetSamplesPerFrame(codecID byte) uint32 {
switch codecID {
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
return 1024
case CodecPCMU, CodecPCMA, CodecPCML, CodecADPCM, CodecSPEEX, CodecG726:
return 160
case CodecMP3:
return 1152
case CodecOpus:
return 960
default:
return 1024
}
}
func IsVideoCodec(id byte) bool {
return id >= CodecMPEG4 && id <= CodecH265
}
func IsAudioCodec(id byte) bool {
return id >= CodecAACRaw && id <= CodecOpus
}
+245 -251
View File
@@ -3,31 +3,71 @@ package tutk
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/wyze/crypto"
"github.com/pion/dtls/v3"
)
const (
MaxPacketSize = 2048
ReadBufferSize = 2 * 1024 * 1024
DiscoTimeout = 5000 * time.Millisecond
DiscoInterval = 100 * time.Millisecond
SessionTimeout = 5000 * time.Millisecond
ReadWaitInterval = 50 * time.Millisecond
magicCC51 = "\x51\xcc" // (wyze specific?)
sdkVersion42 = "\x01\x01\x02\x04" // 4.2.1.1
sdkVersion43 = "\x00\x08\x03\x04" // 4.3.8.0
)
type Conn struct {
const (
cmdDiscoReq uint16 = 0x0601
cmdDiscoRes uint16 = 0x0602
cmdSessionReq uint16 = 0x0402
cmdSessionRes uint16 = 0x0404
cmdDataTX uint16 = 0x0407
cmdDataRX uint16 = 0x0408
cmdKeepaliveReq uint16 = 0x0427
cmdKeepaliveRes uint16 = 0x0428
headerSize = 16
discoBodySize = 72
discoSize = headerSize + discoBodySize
sessionBody = 36
sessionSize = headerSize + sessionBody
)
const (
cmdDiscoCC51 uint16 = 0x1002
cmdKeepaliveCC51 uint16 = 0x1202
cmdDTLSCC51 uint16 = 0x1502
payloadSizeCC51 uint16 = 0x0028
packetSizeCC51 = 52
headerSizeCC51 = 28
authSizeCC51 = 20
keepaliveSizeCC51 = 48
)
const (
magicAVLoginResp uint16 = 0x2100
magicIOCtrl uint16 = 0x7000
magicChannelMsg uint16 = 0x1000
magicACK uint16 = 0x0009
magicAVLogin1 uint16 = 0x0000
magicAVLogin2 uint16 = 0x2000
)
const (
protoVersion uint16 = 0x000c
defaultCaps uint32 = 0x001f07fb
)
const (
iotcChannelMain = 0 // Main AV (we = DTLS Client)
iotcChannelBack = 1 // Backchannel (we = DTLS Server)
)
type DTLSConn struct {
conn *net.UDPConn
addr *net.UDPAddr
frames *FrameHandler
@@ -49,17 +89,15 @@ type Conn struct {
uid string
authKey string
enr string
mac string
psk []byte
rid []byte
// Session
sid []byte
ticket uint16
avResp *AVLoginResponse
sid []byte
ticket uint16
hasTwoWayStreaming bool
// Protocol
newProto bool
isCC51 bool
seq uint16
seqCmd uint16
avSeq uint32
@@ -75,34 +113,32 @@ type Conn struct {
cmdAck func()
}
func Dial(host string, port int, uid, authKey, enr, mac string, verbose bool) (*Conn, error) {
func DialDTLS(host string, port int, uid, authKey, enr string, verbose bool) (*DTLSConn, error) {
udp, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
_ = udp.SetReadBuffer(ReadBufferSize)
_ = udp.SetReadBuffer(2 * 1024 * 1024)
ctx, cancel := context.WithCancel(context.Background())
psk := derivePSK(enr)
psk := DerivePSK(enr)
if port == 0 {
port = DefaultPort
port = 32761
}
c := &Conn{
c := &DTLSConn{
conn: udp,
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: port},
rid: genRandomID(),
uid: uid,
authKey: authKey,
enr: enr,
mac: mac,
psk: psk,
verbose: verbose,
ctx: ctx,
cancel: cancel,
rxSeqStart: 0xffff, // Initialize RX seq for ACK
rxSeqStart: 0xffff,
rxSeqEnd: 0xffff,
}
@@ -130,10 +166,10 @@ func Dial(host string, port int, uid, authKey, enr, mac string, verbose bool) (*
return c, nil
}
func (c *Conn) AVClientStart(timeout time.Duration) error {
randomID := genRandomID()
pkt1 := c.buildAVLoginPacket(MagicAVLogin1, 570, 0x0001, randomID)
pkt2 := c.buildAVLoginPacket(MagicAVLogin2, 572, 0x0000, randomID)
func (c *DTLSConn) AVClientStart(timeout time.Duration) error {
randomID := GenSessionID()
pkt1 := c.msgAVLogin(magicAVLogin1, 570, 0x0001, randomID)
pkt2 := c.msgAVLogin(magicAVLogin2, 572, 0x0000, randomID)
pkt2[20]++ // pkt2 has randomID incremented by 1
if _, err := c.clientConn.Write(pkt1); err != nil {
@@ -155,16 +191,13 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
if !ok {
return io.EOF
}
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == MagicAVLoginResp {
c.avResp = &AVLoginResponse{
ServerType: binary.LittleEndian.Uint32(data[4:]),
Resend: int32(data[29]),
TwoWayStreaming: int32(data[31]),
}
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == magicAVLoginResp {
c.hasTwoWayStreaming = data[31] == 1
ack := c.buildACK()
ack := c.msgACK()
c.clientConn.Write(ack)
// Start ACK sender for continuous streaming
c.wg.Add(1)
go func() {
defer c.wg.Done()
@@ -177,7 +210,7 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
return
case <-ackTicker.C:
if c.clientConn != nil {
ack := c.buildACK()
ack := c.msgACK()
c.clientConn.Write(ack)
}
}
@@ -192,14 +225,9 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
}
}
func (c *Conn) AVServStart() error {
if c.verbose {
fmt.Printf("[DTLS] Waiting for client handshake on channel %d\n", IOTCChannelBack)
fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity)
fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk))
}
conn, err := NewDtlsServer(c, IOTCChannelBack, c.psk)
func (c *DTLSConn) AVServStart() error {
adapter := NewChannelAdapter(c.ctx, iotcChannelBack, c.addr, c.WriteDTLS, c.serverBuf)
conn, err := NewDTLSServer(adapter, c.addr, c.psk)
if err != nil {
return fmt.Errorf("dtls: server handshake failed: %w", err)
}
@@ -209,7 +237,7 @@ func (c *Conn) AVServStart() error {
c.mu.Unlock()
if c.verbose {
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", IOTCChannelBack)
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", iotcChannelBack)
}
// Wait for and respond to AV Login request from camera
@@ -220,10 +248,11 @@ func (c *Conn) AVServStart() error {
return nil
}
func (c *Conn) AVServStop() error {
func (c *DTLSConn) AVServStop() error {
c.mu.Lock()
serverConn := c.serverConn
c.serverConn = nil
// Reset audio TX state
c.audioSeq = 0
c.audioFrameNo = 0
@@ -238,7 +267,7 @@ func (c *Conn) AVServStop() error {
return nil
}
func (c *Conn) AVRecvFrameData() (*Packet, error) {
func (c *DTLSConn) AVRecvFrameData() (*Packet, error) {
select {
case pkt, ok := <-c.frames.Recv():
if !ok {
@@ -250,7 +279,7 @@ func (c *Conn) AVRecvFrameData() (*Packet, error) {
}
}
func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
func (c *DTLSConn) AVSendAudioData(codec byte, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
c.mu.Lock()
conn := c.serverConn
if conn == nil {
@@ -258,7 +287,7 @@ func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32,
return fmt.Errorf("speaker channel not connected")
}
frame := c.buildAudioFrame(payload, timestampUS, codec, sampleRate, channels)
frame := c.msgAudioFrame(payload, timestampUS, codec, sampleRate, channels)
c.mu.Unlock()
@@ -273,35 +302,27 @@ func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32,
return err
}
func (c *Conn) Write(data []byte) error {
// if c.verbose {
// fmt.Printf("[UDP TX] to=%s, len=%d, data:\n%s", c.addr.String(), len(data), hexDump(data))
// }
if c.newProto {
func (c *DTLSConn) Write(data []byte) error {
if c.isCC51 {
_, err := c.conn.WriteToUDP(data, c.addr)
return err
}
_, err := c.conn.WriteToUDP(crypto.TransCodeBlob(data), c.addr)
_, err := c.conn.WriteToUDP(TransCodeBlob(data), c.addr)
return err
}
func (c *Conn) WriteDTLS(payload []byte, channel byte) error {
func (c *DTLSConn) WriteDTLS(payload []byte, channel byte) error {
var frame []byte
if c.newProto {
frame = c.buildNewTxData(payload, channel)
if c.isCC51 {
frame = c.msgTxDataCC51(payload, channel)
} else {
frame = c.buildTxData(payload, channel)
frame = c.msgTxData(payload, channel)
}
// if c.verbose {
// fmt.Printf("[DTLS TX] to=%s, len=%d, channel=%d, data:\n%s", c.addr.String(), len(frame), channel, hexDump(frame))
// }
return c.Write(frame)
}
func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byte) bool) ([]byte, error) {
func (c *DTLSConn) WriteAndWait(req []byte, ok func(res []byte) bool) ([]byte, error) {
var t *time.Timer
t = time.AfterFunc(1, func() {
if err := c.Write(req); err == nil && t != nil {
@@ -310,10 +331,10 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
})
defer t.Stop()
_ = c.conn.SetDeadline(time.Now().Add(timeout))
_ = c.conn.SetDeadline(time.Now().Add(5000 * time.Millisecond))
defer c.conn.SetDeadline(time.Time{})
buf := make([]byte, MaxPacketSize)
buf := make([]byte, 2048)
for {
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
@@ -324,10 +345,10 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
}
var res []byte
if c.newProto {
if c.isCC51 {
res = buf[:n]
} else {
res = crypto.ReverseTransCodeBlob(buf[:n])
res = ReverseTransCodeBlob(buf[:n])
}
if ok(res) {
@@ -337,8 +358,8 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
}
}
func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, timeout time.Duration) ([]byte, error) {
frame := c.buildIOCtrlFrame(payload)
func (c *DTLSConn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, timeout time.Duration) ([]byte, error) {
frame := c.msgIOCtrl(payload)
var t *time.Timer
t = time.AfterFunc(1, func() {
c.mu.RLock()
@@ -362,7 +383,7 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
return nil, io.EOF
}
ack := c.buildACK()
ack := c.msgACK()
c.clientConn.Write(ack)
if len(data) >= 6 {
@@ -376,29 +397,29 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
}
}
func (c *Conn) GetAVLoginResponse() *AVLoginResponse {
return c.avResp
func (c *DTLSConn) HasTwoWayStreaming() bool {
return c.hasTwoWayStreaming
}
func (c *Conn) IsBackchannelReady() bool {
func (c *DTLSConn) IsBackchannelReady() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.serverConn != nil
}
func (c *Conn) RemoteAddr() *net.UDPAddr {
func (c *DTLSConn) RemoteAddr() *net.UDPAddr {
return c.addr
}
func (c *Conn) LocalAddr() *net.UDPAddr {
func (c *DTLSConn) LocalAddr() *net.UDPAddr {
return c.conn.LocalAddr().(*net.UDPAddr)
}
func (c *Conn) SetDeadline(t time.Time) error {
func (c *DTLSConn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *Conn) Close() error {
func (c *DTLSConn) Close() error {
c.cancel()
c.mu.Lock()
@@ -416,27 +437,27 @@ func (c *Conn) Close() error {
return c.conn.Close()
}
func (c *Conn) Error() error {
func (c *DTLSConn) Error() error {
if c.err != nil {
return c.err
}
return io.EOF
}
func (c *Conn) discovery() error {
c.sid = make([]byte, 8)
rand.Read(c.sid)
func (c *DTLSConn) discovery() error {
c.sid = GenSessionID()
oldPkt := crypto.TransCodeBlob(c.buildDisco(1))
newPkt := c.buildNewDisco(0, 0, false)
buf := make([]byte, MaxPacketSize)
deadline := time.Now().Add(DiscoTimeout)
pktIOTC := TransCodeBlob(c.msgDisco(1))
pktCC51 := c.msgDiscoCC51(0, 0, false)
buf := make([]byte, 2048)
deadline := time.Now().Add(5000 * time.Millisecond)
for time.Now().Before(deadline) {
c.conn.WriteToUDP(oldPkt, c.addr)
c.conn.WriteToUDP(newPkt, c.addr)
c.conn.WriteToUDP(pktIOTC, c.addr)
c.conn.WriteToUDP(pktCC51, c.addr)
c.conn.SetReadDeadline(time.Now().Add(DiscoInterval))
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
continue
@@ -445,59 +466,54 @@ func (c *Conn) discovery() error {
continue
}
// NEW protocol
if n >= NewPacketSize && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
if binary.LittleEndian.Uint16(buf[4:]) == CmdNewDisco {
c.addr, c.newProto, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])
// CC51 protocol
if n >= packetSizeCC51 && string(buf[:2]) == magicCC51 {
if binary.LittleEndian.Uint16(buf[4:]) == cmdDiscoCC51 {
c.addr, c.isCC51, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])
if n >= 24 {
copy(c.sid, buf[16:24])
}
return c.newDiscoDone()
return c.discoDoneCC51()
}
continue
}
// OLD protocol
data := crypto.ReverseTransCodeBlob(buf[:n])
if len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == CmdDiscoRes {
c.addr, c.newProto = addr, false
return c.oldDiscoDone()
// IOTC Protocol (Basis)
data := ReverseTransCodeBlob(buf[:n])
if len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == cmdDiscoRes {
c.addr, c.isCC51 = addr, false
return c.discoDone()
}
}
return fmt.Errorf("discovery timeout")
}
func (c *Conn) oldDiscoDone() error {
c.Write(c.buildDisco(2))
func (c *DTLSConn) discoDone() error {
c.Write(c.msgDisco(2))
time.Sleep(100 * time.Millisecond)
_, err := c.WriteAndWait(c.buildSession(), SessionTimeout, func(res []byte) bool {
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == CmdSessionRes
_, err := c.WriteAndWait(c.msgSession(), func(res []byte) bool {
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == cmdSessionRes
})
return err
}
func (c *Conn) newDiscoDone() error {
_, err := c.WriteAndWait(c.buildNewDisco(2, c.ticket, false), SessionTimeout, func(res []byte) bool {
if len(res) < NewPacketSize || binary.LittleEndian.Uint16(res[:2]) != MagicNewProto {
func (c *DTLSConn) discoDoneCC51() error {
_, err := c.WriteAndWait(c.msgDiscoCC51(2, c.ticket, false), func(res []byte) bool {
if len(res) < packetSizeCC51 || string(res[:2]) != magicCC51 {
return false
}
cmd := binary.LittleEndian.Uint16(res[4:])
dir := binary.LittleEndian.Uint16(res[8:])
seq := binary.LittleEndian.Uint16(res[12:])
return cmd == CmdNewDisco && dir == 0xFFFF && seq == 3
return cmd == cmdDiscoCC51 && dir == 0xFFFF && seq == 3
})
return err
}
func (c *Conn) connect() error {
if c.verbose {
fmt.Printf("[DTLS] Starting client handshake on channel %d\n", IOTCChannelMain)
fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity)
fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk))
}
conn, err := NewDtlsClient(c, IOTCChannelMain, c.psk)
func (c *DTLSConn) connect() error {
adapter := NewChannelAdapter(c.ctx, iotcChannelMain, c.addr, c.WriteDTLS, c.clientBuf)
conn, err := NewDTLSClient(adapter, c.addr, c.psk)
if err != nil {
return fmt.Errorf("dtls: client create failed: %w", err)
}
@@ -507,13 +523,13 @@ func (c *Conn) connect() error {
c.mu.Unlock()
if c.verbose {
fmt.Printf("[DTLS] Client created for channel %d\n", IOTCChannelMain)
fmt.Printf("[DTLS] Client created for channel %d\n", iotcChannelMain)
}
return nil
}
func (c *Conn) worker() {
func (c *DTLSConn) worker() {
defer c.wg.Done()
buf := make([]byte, 2048)
@@ -538,15 +554,11 @@ func (c *Conn) worker() {
data := buf[:n]
magic := binary.LittleEndian.Uint16(data)
// if c.verbose {
// fmt.Printf("[DTLS RX] from=%s, len=%d, data:\n%s", c.addr.String(), n, hexDump(data))
// }
switch magic {
case MagicAVLoginResp:
case magicAVLoginResp:
c.queue(c.rawCmd, data)
case MagicIOCtrl:
case magicIOCtrl:
if len(data) >= 32 {
for i := 32; i+2 < len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
@@ -556,7 +568,7 @@ func (c *Conn) worker() {
}
}
case MagicChannelMsg:
case magicChannelMsg:
if len(data) >= 36 && data[16] == 0x00 {
for i := 36; i+2 < len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
@@ -566,7 +578,7 @@ func (c *Conn) worker() {
}
}
case ProtoVersion:
case protoVersion:
if len(data) >= 8 {
// Extract seq number at byte 4-5 (uint16 of uint32 AVSeq)
seq := binary.LittleEndian.Uint16(data[4:])
@@ -589,7 +601,7 @@ func (c *Conn) worker() {
}
}
case MagicACK:
case magicACK:
c.mu.RLock()
ack := c.cmdAck
c.mu.RUnlock()
@@ -606,9 +618,10 @@ func (c *Conn) worker() {
}
}
func (c *Conn) reader() {
func (c *DTLSConn) reader() {
defer c.wg.Done()
buf := make([]byte, MaxPacketSize)
buf := make([]byte, 2048)
for {
select {
@@ -626,10 +639,6 @@ func (c *Conn) reader() {
return
}
// if c.verbose {
// fmt.Printf("[UDP RX] from=%s, len=%d, data:\n%s", addr.String(), n, hexDump(buf[:n]))
// }
if !addr.IP.Equal(c.addr.IP) {
if c.verbose {
fmt.Printf("Ignored packet from unknown IP: %s\n", addr.IP.String())
@@ -640,47 +649,47 @@ func (c *Conn) reader() {
c.addr.Port = addr.Port
}
// NEW protocol (0xCC51)
if c.newProto && n >= 12 && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
// CC51 Protocol
if c.isCC51 && n >= 12 && string(buf[:2]) == magicCC51 {
cmd := binary.LittleEndian.Uint16(buf[4:])
switch cmd {
case CmdNewKeepalive:
if n >= NewKeepaliveSize {
_ = c.Write(c.buildNewKeepalive())
case cmdKeepaliveCC51:
if n >= keepaliveSizeCC51 {
_ = c.Write(c.msgKeepaliveCC51())
}
case CmdNewDTLS:
if n >= NewHeaderSize+NewAuthSize {
case cmdDTLSCC51:
if n >= headerSizeCC51+authSizeCC51 {
ch := byte(binary.LittleEndian.Uint16(buf[12:]) >> 8)
dtls := buf[NewHeaderSize : n-NewAuthSize]
dtlsData := buf[headerSizeCC51 : n-authSizeCC51]
switch ch {
case IOTCChannelMain:
c.queue(c.clientBuf, dtls)
case IOTCChannelBack:
c.queue(c.serverBuf, dtls)
case iotcChannelMain:
c.queue(c.clientBuf, dtlsData)
case iotcChannelBack:
c.queue(c.serverBuf, dtlsData)
}
}
}
continue
}
// OLD protocol (TransCode)
data := crypto.ReverseTransCodeBlob(buf[:n])
// IOTC Protocol (Basis)
data := ReverseTransCodeBlob(buf[:n])
if len(data) < 16 {
continue
}
switch binary.LittleEndian.Uint16(data[8:]) {
case CmdKeepaliveRes:
case cmdKeepaliveRes:
if len(data) > 24 {
_ = c.Write(c.buildKeepAlive(data[16:]))
_ = c.Write(c.msgKeepalive(data[16:]))
}
case CmdDataRX:
case cmdDataRX:
if len(data) > 28 {
ch := data[14]
switch ch {
case IOTCChannelMain:
case iotcChannelMain:
c.queue(c.clientBuf, data[28:])
case IOTCChannelBack:
case iotcChannelBack:
c.queue(c.serverBuf, data[28:])
}
}
@@ -688,7 +697,7 @@ func (c *Conn) reader() {
}
}
func (c *Conn) queue(ch chan []byte, data []byte) {
func (c *DTLSConn) queue(ch chan []byte, data []byte) {
b := make([]byte, len(data))
copy(b, data)
select {
@@ -702,7 +711,7 @@ func (c *Conn) queue(ch chan []byte, data []byte) {
}
}
func (c *Conn) handleSpeakerAVLogin() error {
func (c *DTLSConn) handleSpeakerAVLogin() error {
if c.verbose {
fmt.Printf("[SPEAK] Waiting for AV Login request from camera...\n")
}
@@ -723,7 +732,7 @@ func (c *Conn) handleSpeakerAVLogin() error {
}
checksum := binary.LittleEndian.Uint32(buf[20:])
resp := c.buildAVLoginResponse(checksum)
resp := c.msgAVLoginResponse(checksum)
if c.verbose {
fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp))
@@ -751,16 +760,16 @@ func (c *Conn) handleSpeakerAVLogin() error {
return nil
}
func (c *Conn) buildDisco(stage byte) []byte {
b := make([]byte, OldDiscoSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], OldDiscoBodySize) // body size
binary.LittleEndian.PutUint16(b[8:], CmdDiscoReq) // 0x0601
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
body := b[OldHeaderSize:]
copy(body[:UIDSize], c.uid)
copy(body[36:], "\x01\x01\x02\x04") // unknown
copy(body[40:], c.rid)
func (c *DTLSConn) msgDisco(stage byte) []byte {
b := make([]byte, discoSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], discoBodySize) // body size
binary.LittleEndian.PutUint16(b[8:], cmdDiscoReq) // 0x0601
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
body := b[headerSize:]
copy(body[:20], c.uid)
copy(body[36:], sdkVersion42) // SDK 4.2.1.1
copy(body[40:], c.sid)
body[48] = stage
if stage == 1 && len(c.authKey) > 0 {
copy(body[58:], c.authKey)
@@ -768,69 +777,67 @@ func (c *Conn) buildDisco(stage byte) []byte {
return b
}
func (c *Conn) buildNewDisco(seq, ticket uint16, isResponse bool) []byte {
b := make([]byte, NewPacketSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewDisco) // 0x1002
binary.LittleEndian.PutUint16(b[6:], NewPayloadSize) // 40 bytes
func (c *DTLSConn) msgDiscoCC51(seq, ticket uint16, isResponse bool) []byte {
b := make([]byte, packetSizeCC51)
copy(b[:2], magicCC51)
binary.LittleEndian.PutUint16(b[4:], cmdDiscoCC51) // 0x1002
binary.LittleEndian.PutUint16(b[6:], payloadSizeCC51) // 40 bytes
if isResponse {
binary.LittleEndian.PutUint16(b[8:], 0xFFFF) // response
}
binary.LittleEndian.PutUint16(b[12:], seq)
binary.LittleEndian.PutUint16(b[14:], ticket)
copy(b[16:24], c.sid)
copy(b[24:32], "\x00\x08\x03\x04\x1d\x00\x00\x00") // SDK 4.3.8.0
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
copy(b[24:28], sdkVersion43) // SDK 4.3.8.0
b[28] = 0x1d // unknown field (capability/build flag?)
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
h.Write(b[:32])
copy(b[32:52], h.Sum(nil))
return b
}
func (c *Conn) buildNewKeepalive() []byte {
func (c *DTLSConn) msgKeepaliveCC51() []byte {
c.kaSeq += 2
b := make([]byte, NewKeepaliveSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewKeepalive) // 0x1202
binary.LittleEndian.PutUint16(b[6:], 0x0024) // 36 bytes payload
binary.LittleEndian.PutUint32(b[16:], c.kaSeq) // counter
copy(b[20:28], c.sid) // session ID
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
b := make([]byte, keepaliveSizeCC51)
copy(b[:2], magicCC51)
binary.LittleEndian.PutUint16(b[4:], cmdKeepaliveCC51) // 0x1202
binary.LittleEndian.PutUint16(b[6:], 0x0024) // 36 bytes payload
binary.LittleEndian.PutUint32(b[16:], c.kaSeq) // counter
copy(b[20:28], c.sid) // session ID
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
h.Write(b[:28])
copy(b[28:48], h.Sum(nil))
return b
}
func (c *Conn) buildSession() []byte {
b := make([]byte, OldSessionSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], OldSessionBody) // body size
binary.LittleEndian.PutUint16(b[8:], CmdSessionReq) // 0x0402
binary.LittleEndian.PutUint16(b[10:], 0x0033) // flags
body := b[OldHeaderSize:]
copy(body[:UIDSize], c.uid)
copy(body[UIDSize:], c.rid)
func (c *DTLSConn) msgSession() []byte {
b := make([]byte, sessionSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], sessionBody) // body size
binary.LittleEndian.PutUint16(b[8:], cmdSessionReq) // 0x0402
binary.LittleEndian.PutUint16(b[10:], 0x0033) // flags
body := b[headerSize:]
copy(body[:20], c.uid)
copy(body[20:], c.sid)
binary.LittleEndian.PutUint32(body[32:], uint32(time.Now().Unix()))
return b
}
func (c *Conn) buildAVLoginPacket(magic uint16, size int, flags uint16, randomID []byte) []byte {
func (c *DTLSConn) msgAVLogin(magic uint16, size int, flags uint16, randomID []byte) []byte {
b := make([]byte, size)
binary.LittleEndian.PutUint16(b, magic)
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
binary.LittleEndian.PutUint16(b[2:], protoVersion)
binary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size
binary.LittleEndian.PutUint16(b[18:], flags)
copy(b[20:], randomID[:4])
copy(b[24:], DefaultUser) // username
copy(b[280:], c.enr) // password/ENR
// binary.LittleEndian.PutUint32(b[536:], 1) // resend enabled
copy(b[24:], "admin") // username
copy(b[280:], c.enr) // password/ENR
binary.LittleEndian.PutUint32(b[540:], 4) // security_mode ?
binary.LittleEndian.PutUint32(b[552:], DefaultCaps) // capabilities
binary.LittleEndian.PutUint32(b[552:], defaultCaps) // capabilities
return b
}
func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
func (c *DTLSConn) msgAVLoginResponse(checksum uint32) []byte {
b := make([]byte, 60)
binary.LittleEndian.PutUint16(b, 0x2100) // magic
binary.LittleEndian.PutUint16(b[2:], 0x000c) // version
@@ -840,13 +847,13 @@ func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
b[29] = 0x01 // enable flag
b[31] = 0x01 // two-way streaming
binary.LittleEndian.PutUint32(b[36:], 0x04) // buffer config
binary.LittleEndian.PutUint32(b[40:], DefaultCaps)
binary.LittleEndian.PutUint32(b[40:], defaultCaps)
binary.LittleEndian.PutUint16(b[54:], 0x0003) // channel info
binary.LittleEndian.PutUint16(b[56:], 0x0002)
return b
}
func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte {
func (c *DTLSConn) msgAudioFrame(payload []byte, timestampUS uint32, codec byte, sampleRate uint32, channels uint8) []byte {
c.audioSeq++
c.audioFrameNo++
prevFrame := uint32(0)
@@ -860,7 +867,7 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
// Outer header (36 bytes)
b[0] = ChannelAudio // 0x03
b[1] = FrameTypeStartAlt // 0x09
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
binary.LittleEndian.PutUint16(b[2:], protoVersion)
binary.LittleEndian.PutUint32(b[4:], c.audioSeq)
binary.LittleEndian.PutUint32(b[8:], timestampUS)
if c.audioFrameNo == 1 {
@@ -880,54 +887,65 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
binary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)
copy(b[36:], payload) // Payload + FrameInfo
fi := b[36+len(payload):]
binary.LittleEndian.PutUint16(fi, codec)
fi[2] = BuildAudioFlags(sampleRate, true, channels == 2)
fi[0] = codec // Codec ID (low byte)
fi[1] = 0 // Codec ID (high byte, unused)
// Audio flags: [3:2]=sampleRateIdx [1]=16bit [0]=stereo
var srIdx uint8 = 3 // default 16kHz
for i, rate := range sampleRates {
if rate == sampleRate {
srIdx = uint8(i)
break
}
}
fi[2] = (srIdx << 2) | 0x02 // 16-bit always set
if channels == 2 {
fi[2] |= 0x01
}
fi[4] = 1 // online
binary.LittleEndian.PutUint32(fi[12:], (c.audioFrameNo-1)*GetSamplesPerFrame(codec)*1000/sampleRate)
return b
}
func (c *Conn) buildTxData(payload []byte, channel byte) []byte {
func (c *DTLSConn) msgTxData(payload []byte, channel byte) []byte {
bodySize := 12 + len(payload)
b := make([]byte, 16+bodySize)
copy(b, "\x04\x02\x1a\x0b") // marker + mode=data
binary.LittleEndian.PutUint16(b[4:], uint16(bodySize)) // body size
binary.LittleEndian.PutUint16(b[6:], c.seq) // sequence
c.seq++
binary.LittleEndian.PutUint16(b[8:], CmdDataTX) // 0x0407
binary.LittleEndian.PutUint16(b[8:], cmdDataTX) // 0x0407
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
copy(b[12:], c.rid[:2]) // rid[0:2]
copy(b[12:], c.sid[:2]) // rid[0:2]
b[14] = channel // channel
b[15] = 0x01 // marker
binary.LittleEndian.PutUint32(b[16:], 0x0000000c) // const
copy(b[20:], c.rid[:8]) // rid
copy(b[20:], c.sid[:8]) // rid
copy(b[28:], payload)
return b
}
func (c *Conn) buildNewTxData(payload []byte, channel byte) []byte {
payloadSize := uint16(16 + len(payload) + NewAuthSize)
b := make([]byte, NewHeaderSize+len(payload)+NewAuthSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewDTLS) // 0x1502
func (c *DTLSConn) msgTxDataCC51(payload []byte, channel byte) []byte {
payloadSize := uint16(16 + len(payload) + authSizeCC51)
b := make([]byte, headerSizeCC51+len(payload)+authSizeCC51)
copy(b[:2], magicCC51)
binary.LittleEndian.PutUint16(b[4:], cmdDTLSCC51) // 0x1502
binary.LittleEndian.PutUint16(b[6:], payloadSize)
binary.LittleEndian.PutUint16(b[12:], uint16(0x0010)|(uint16(channel)<<8)) // channel in high byte
binary.LittleEndian.PutUint16(b[14:], c.ticket)
copy(b[16:24], c.sid)
binary.LittleEndian.PutUint32(b[24:], 1) // const
copy(b[NewHeaderSize:], payload)
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
h.Write(b[:NewHeaderSize])
copy(b[NewHeaderSize+len(payload):], h.Sum(nil))
copy(b[headerSizeCC51:], payload)
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
h.Write(b[:headerSizeCC51])
copy(b[headerSizeCC51+len(payload):], h.Sum(nil))
return b
}
func (c *Conn) buildACK() []byte {
func (c *DTLSConn) msgACK() []byte {
c.ackFlags++
b := make([]byte, 24)
binary.LittleEndian.PutUint16(b[0:], MagicACK) // 0x0009
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // 0x000c
binary.LittleEndian.PutUint16(b[0:], magicACK) // 0x0009
binary.LittleEndian.PutUint16(b[2:], protoVersion) // 0x000c
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // TX seq
c.avSeq++
binary.LittleEndian.PutUint16(b[8:], c.rxSeqStart) // RX start (last acked)
@@ -942,11 +960,11 @@ func (c *Conn) buildACK() []byte {
return b
}
func (c *Conn) buildKeepAlive(incoming []byte) []byte {
func (c *DTLSConn) msgKeepalive(incoming []byte) []byte {
b := make([]byte, 24)
copy(b, "\x04\x02\x1a\x0a") // marker + mode
binary.LittleEndian.PutUint16(b[4:], 8) // body size
binary.LittleEndian.PutUint16(b[8:], CmdKeepaliveReq) // 0x0427
binary.LittleEndian.PutUint16(b[8:], cmdKeepaliveReq) // 0x0427
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
if len(incoming) >= 8 {
copy(b[16:], incoming[:8]) // echo payload
@@ -954,13 +972,13 @@ func (c *Conn) buildKeepAlive(incoming []byte) []byte {
return b
}
func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
func (c *DTLSConn) msgIOCtrl(payload []byte) []byte {
b := make([]byte, 40+len(payload))
binary.LittleEndian.PutUint16(b, ProtoVersion) // magic
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // version
binary.LittleEndian.PutUint16(b, protoVersion) // magic
binary.LittleEndian.PutUint16(b[2:], protoVersion) // version
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // av seq
c.avSeq++
binary.LittleEndian.PutUint16(b[16:], MagicIOCtrl) // 0x7000
binary.LittleEndian.PutUint16(b[16:], magicIOCtrl) // 0x7000
binary.LittleEndian.PutUint16(b[18:], c.seqCmd) // sub channel
binary.LittleEndian.PutUint32(b[20:], 1) // ioctl seq
binary.LittleEndian.PutUint32(b[24:], uint32(len(payload)+4)) // payload size
@@ -971,30 +989,6 @@ func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
return b
}
func derivePSK(enr string) []byte {
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
// contains a 0x00 byte, the PSK is truncated at that position.
// bytes after the first 0x00 are padded with zeros to make a 32-byte key.
hash := sha256.Sum256([]byte(enr))
pskLen := 32
for i := range 32 {
if hash[i] == 0x00 {
pskLen = i
break
}
}
psk := make([]byte, 32)
copy(psk[:pskLen], hash[:pskLen])
return psk
}
func genRandomID() []byte {
b := make([]byte, 8)
_, _ = rand.Read(b)
return b
}
func hexDump(data []byte) string {
const maxBytes = 650
totalLen := len(data)
+102
View File
@@ -50,6 +50,34 @@ func ReverseTransCodePartial(dst, src []byte) []byte {
return dst
}
func ReverseTransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return ReverseTransCodePartial(nil, src)
}
dst := make([]byte, len(src))
header := ReverseTransCodePartial(nil, src[:16])
copy(dst, header)
if len(src) > 16 {
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
remaining := len(src) - 16
decryptLen := min(remaining, 48)
if decryptLen > 0 {
decrypted := ReverseTransCodePartial(nil, src[16:16+decryptLen])
copy(dst[16:], decrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full decryption
decrypted := ReverseTransCodePartial(nil, src[16:])
copy(dst[16:], decrypted)
}
}
return dst
}
func TransCodePartial(dst, src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
@@ -92,6 +120,34 @@ func TransCodePartial(dst, src []byte) []byte {
return dst
}
func TransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return TransCodePartial(nil, src)
}
dst := make([]byte, len(src))
header := TransCodePartial(nil, src[:16])
copy(dst, header)
if len(src) > 16 {
if src[3]&1 != 0 { // Partial encryption
remaining := len(src) - 16
encryptLen := min(remaining, 48)
if encryptLen > 0 {
encrypted := TransCodePartial(nil, src[16:16+encryptLen])
copy(dst[16:], encrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full encryption
encrypted := TransCodePartial(nil, src[16:])
copy(dst[16:], encrypted)
}
}
return dst
}
func swap(dst, src []byte, n int) {
switch n {
case 2:
@@ -175,3 +231,49 @@ func XXTEADecrypt(dst, src, key []byte) {
dst = dst[4:]
}
}
func XXTEADecryptVar(data, key []byte) []byte {
if len(data) < 8 || len(key) < 16 {
return nil
}
k := make([]uint32, 4)
for i := range 4 {
k[i] = binary.LittleEndian.Uint32(key[i*4:])
}
n := max(len(data)/4, 2)
v := make([]uint32, n)
for i := 0; i < len(data)/4; i++ {
v[i] = binary.LittleEndian.Uint32(data[i*4:])
}
rounds := 6 + 52/n
sum := uint32(rounds) * delta
y := v[0]
for rounds > 0 {
e := (sum >> 2) & 3
for p := n - 1; p > 0; p-- {
z := v[p-1]
v[p] -= xxteaMX(sum, y, z, p, e, k)
y = v[p]
}
z := v[n-1]
v[0] -= xxteaMX(sum, y, z, 0, e, k)
y = v[0]
sum -= delta
rounds--
}
result := make([]byte, n*4)
for i := range n {
binary.LittleEndian.PutUint32(result[i*4:], v[i])
}
return result[:len(data)]
}
func xxteaMX(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
}
+34 -27
View File
@@ -1,6 +1,7 @@
package tutk
import (
"context"
"net"
"sync"
"time"
@@ -8,22 +9,26 @@ import (
"github.com/pion/dtls/v3"
)
func NewDtlsClient(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
adapter := &ChannelAdapter{conn: c, channel: channel}
return dtls.Client(adapter, c.addr, buildDtlsConfig(psk, false))
type DTLSConfig struct {
PSK []byte
Identity string
IsServer bool
}
func NewDtlsServer(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
adapter := &ChannelAdapter{conn: c, channel: channel}
return dtls.Server(adapter, c.addr, buildDtlsConfig(psk, true))
func NewDTLSClient(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
return dtls.Client(adapter, addr, buildDTLSConfig(psk, false))
}
func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config {
func NewDTLSServer(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
return dtls.Server(adapter, addr, buildDTLSConfig(psk, true))
}
func buildDTLSConfig(psk []byte, isServer bool) *dtls.Config {
config := &dtls.Config{
PSK: func(hint []byte) ([]byte, error) {
return psk, nil
},
PSKIdentityHint: []byte(PSKIdentity),
PSKIdentityHint: []byte("AUTHPWD_admin"),
InsecureSkipVerify: true,
InsecureSkipVerifyHello: true,
MTU: 1200,
@@ -41,21 +46,26 @@ func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config {
}
type ChannelAdapter struct {
conn *Conn
channel uint8
ctx context.Context
channel uint8
writeFn func([]byte, uint8) error
readChan chan []byte
addr net.Addr
mu sync.Mutex
readDeadline time.Time
}
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
var buf chan []byte
if a.channel == IOTCChannelMain {
buf = a.conn.clientBuf
} else {
buf = a.conn.serverBuf
func NewChannelAdapter(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte) *ChannelAdapter {
return &ChannelAdapter{
ctx: ctx,
channel: channel,
addr: addr,
writeFn: writeFn,
readChan: readChan,
}
}
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
a.mu.Lock()
deadline := a.readDeadline
a.mu.Unlock()
@@ -70,25 +80,25 @@ func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
defer timer.Stop()
select {
case data := <-buf:
return copy(p, data), a.conn.addr, nil
case data := <-a.readChan:
return copy(p, data), a.addr, nil
case <-timer.C:
return 0, nil, &timeoutError{}
case <-a.conn.ctx.Done():
case <-a.ctx.Done():
return 0, nil, net.ErrClosed
}
}
select {
case data := <-buf:
return copy(p, data), a.conn.addr, nil
case <-a.conn.ctx.Done():
case data := <-a.readChan:
return copy(p, data), a.addr, nil
case <-a.ctx.Done():
return 0, nil, net.ErrClosed
}
}
func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {
if err := a.conn.WriteDTLS(p, a.channel); err != nil {
if err := a.writeFn(p, a.channel); err != nil {
return 0, err
}
return len(p), nil
@@ -96,21 +106,18 @@ func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {
func (a *ChannelAdapter) Close() error { return nil }
func (a *ChannelAdapter) LocalAddr() net.Addr { return &net.UDPAddr{} }
func (a *ChannelAdapter) SetDeadline(t time.Time) error {
a.mu.Lock()
a.readDeadline = t
a.mu.Unlock()
return nil
}
func (a *ChannelAdapter) SetReadDeadline(t time.Time) error {
a.mu.Lock()
a.readDeadline = t
a.mu.Unlock()
return nil
}
func (a *ChannelAdapter) SetWriteDeadline(time.Time) error { return nil }
type timeoutError struct{}
+35 -79
View File
@@ -25,18 +25,7 @@ const (
ChannelPVideo uint8 = 0x07
)
const (
ResTierLow uint8 = 1 // 360P/SD
ResTierHigh uint8 = 4 // HD/2K
)
const (
Bitrate360P uint8 = 30
BitrateHD uint8 = 100
Bitrate2K uint8 = 200
)
const FrameInfoSize = 40
const frameInfoSize = 40
// FrameInfo - Wyze extended FRAMEINFO (40 bytes at end of packet)
// Video: 40 bytes, Audio: 16 bytes (uses same struct, fields 16+ are zero)
@@ -56,7 +45,7 @@ const FrameInfoSize = 40
// 24-35 12 DeviceID - MAC address (ASCII) - video only
// 36-39 4 Padding - Always 0 - video only
type FrameInfo struct {
CodecID uint16 // 0-1
CodecID byte // 0 (only low byte used)
Flags uint8 // 2
CamIndex uint8 // 3
OnlineNum uint8 // 4
@@ -73,22 +62,12 @@ func (fi *FrameInfo) IsKeyframe() bool {
return fi.Flags == 0x01
}
func (fi *FrameInfo) Resolution() string {
switch fi.Bitrate {
case Bitrate360P:
return "360P"
case BitrateHD:
return "HD"
case Bitrate2K:
return "2K"
default:
return "unknown"
}
}
func (fi *FrameInfo) SampleRate() uint32 {
idx := (fi.Flags >> 2) & 0x0F
return uint32(SampleRateValue(idx))
if idx < uint8(len(sampleRates)) {
return sampleRates[idx]
}
return 16000
}
func (fi *FrameInfo) Channels() uint8 {
@@ -98,24 +77,16 @@ func (fi *FrameInfo) Channels() uint8 {
return 1
}
func (fi *FrameInfo) IsVideo() bool {
return IsVideoCodec(fi.CodecID)
}
func (fi *FrameInfo) IsAudio() bool {
return IsAudioCodec(fi.CodecID)
}
func ParseFrameInfo(data []byte) *FrameInfo {
if len(data) < FrameInfoSize {
if len(data) < frameInfoSize {
return nil
}
offset := len(data) - FrameInfoSize
offset := len(data) - frameInfoSize
fi := data[offset:]
return &FrameInfo{
CodecID: binary.LittleEndian.Uint16(fi),
CodecID: fi[0],
Flags: fi[2],
CamIndex: fi[3],
OnlineNum: fi[4],
@@ -131,7 +102,7 @@ func ParseFrameInfo(data []byte) *FrameInfo {
type Packet struct {
Channel uint8
Codec uint16
Codec byte
Timestamp uint32
Payload []byte
IsKeyframe bool
@@ -140,14 +111,6 @@ type Packet struct {
Channels uint8
}
func (p *Packet) IsVideo() bool {
return p.Channel == ChannelIVideo || p.Channel == ChannelPVideo
}
func (p *Packet) IsAudio() bool {
return p.Channel == ChannelAudio
}
type PacketHeader struct {
Channel byte
FrameType byte
@@ -347,7 +310,7 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
frameType := data[1]
headerSize := 28
frameInfoSize := 0
fiSize := 0
switch frameType {
case FrameTypeStart:
@@ -357,17 +320,17 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
if len(data) >= 22 {
pktTotal := binary.LittleEndian.Uint16(data[20:])
if pktTotal == 1 {
frameInfoSize = FrameInfoSize
fiSize = frameInfoSize
}
}
case FrameTypeCont, FrameTypeContAlt:
headerSize = 28
case FrameTypeEndSingle, FrameTypeEndMulti:
headerSize = 28
frameInfoSize = FrameInfoSize
fiSize = frameInfoSize
case FrameTypeEndExt:
headerSize = 36
frameInfoSize = FrameInfoSize
fiSize = frameInfoSize
default:
headerSize = 28
}
@@ -376,11 +339,11 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
return nil, nil
}
if frameInfoSize == 0 {
if fiSize == 0 {
return data[headerSize:], nil
}
if len(data) < headerSize+frameInfoSize {
if len(data) < headerSize+fiSize {
return data[headerSize:], nil
}
@@ -395,7 +358,7 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
}
if validCodec {
payload := data[headerSize : len(data)-frameInfoSize]
payload := data[headerSize : len(data)-fiSize]
return payload, fi
}
@@ -421,7 +384,7 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
cs.pktTotal = hdr.PktTotal
}
// Sequential check: if packet index doesn't match expected, reset (data loss)
// If packet index doesn't match expected, reset (data loss)
if hdr.PktIdx != cs.waitSeq {
fmt.Printf("[OOO] ch=0x%02x #%d frameType=0x%02x pktTotal=%d expected pkt %d, got %d - reset\n",
channel, hdr.FrameNo, hdr.FrameType, hdr.PktTotal, cs.waitSeq, hdr.PktIdx)
@@ -434,7 +397,6 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
cs.hasStarted = true
}
// Append payload (simple sequential accumulation)
cs.waitData = append(cs.waitData, payload...)
cs.waitSeq++
@@ -444,16 +406,13 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
}
// Check if frame is complete
if cs.waitSeq == cs.pktTotal && cs.frameInfo != nil {
h.emitVideo(channel, cs)
cs.reset()
if cs.waitSeq != cs.pktTotal || cs.frameInfo == nil {
return
}
}
func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
fi := cs.frameInfo
fi = cs.frameInfo
defer cs.reset()
// Size validation
if fi.PayloadSize > 0 && uint32(len(cs.waitData)) != fi.PayloadSize {
fmt.Printf("[SIZE] ch=0x%02x #%d mismatch: expected %d, got %d\n",
channel, cs.frameNo, fi.PayloadSize, len(cs.waitData))
@@ -467,13 +426,9 @@ func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
accumUS := h.videoTS.update(fi.Timestamp)
rtpTS := uint32(accumUS * 90000 / 1000000)
// Copy payload (buffer will be reused)
payload := make([]byte, len(cs.waitData))
copy(payload, cs.waitData)
pkt := &Packet{
Channel: channel,
Payload: payload,
Payload: append([]byte{}, cs.waitData...),
Codec: fi.CodecID,
Timestamp: rtpTS,
IsKeyframe: fi.IsKeyframe(),
@@ -485,10 +440,10 @@ func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
if fi.IsKeyframe() {
frameType = "KEY"
}
fmt.Printf("[OK] ch=0x%02x #%d %s %s size=%d\n",
channel, fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload))
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x [3]=%d [4]=%d\n",
fi.CodecID, CodecName(fi.CodecID), fi.Flags, fi.CamIndex, fi.OnlineNum)
fmt.Printf("[OK] ch=0x%02x #%d codec=0x%02x %s size=%d\n",
channel, fi.FrameNo, fi.CodecID, frameType, len(pkt.Payload))
fmt.Printf(" [0-1]codec=0x%02x [2]flags=0x%x [3]=%d [4]=%d\n",
fi.CodecID, fi.Flags, fi.CamIndex, fi.OnlineNum)
fmt.Printf(" [5]=%d [6]=%d [7]=%d [8-11]ts=%d\n",
fi.FPS, fi.ResTier, fi.Bitrate, fi.Timestamp)
fmt.Printf(" [12-15]=0x%x [16-19]payload=%d [20-23]frameNo=%d\n",
@@ -509,7 +464,7 @@ func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
var channels uint8
switch fi.CodecID {
case AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACLATM, AudioCodecAACWyze:
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
sampleRate, channels = parseAudioParams(payload, fi)
default:
sampleRate = fi.SampleRate()
@@ -537,10 +492,10 @@ func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
if fi.Flags&0x02 != 0 {
bits = 16
}
fmt.Printf("[OK] Audio #%d %s size=%d\n",
fi.FrameNo, AudioCodecName(fi.CodecID), len(payload))
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x(%dHz/%dbit/%dch)\n",
fi.CodecID, AudioCodecName(fi.CodecID), fi.Flags, sampleRate, bits, channels)
fmt.Printf("[OK] Audio #%d codec=0x%02x size=%d\n",
fi.FrameNo, fi.CodecID, len(payload))
fmt.Printf(" [0-1]codec=0x%02x [2]flags=0x%x(%dHz/%dbit/%dch)\n",
fi.CodecID, fi.Flags, sampleRate, bits, channels)
fmt.Printf(" [8-11]ts=%d [12-15]=0x%x rtp_ts=%d\n",
fi.Timestamp, fi.SessionID, rtpTS)
fmt.Printf(" hex: %s\n", dumpHex(fi))
@@ -589,8 +544,9 @@ func parseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channel
}
func dumpHex(fi *FrameInfo) string {
b := make([]byte, FrameInfoSize)
binary.LittleEndian.PutUint16(b[0:], fi.CodecID)
b := make([]byte, frameInfoSize)
b[0] = fi.CodecID
b[1] = 0 // High byte (unused)
b[2] = fi.Flags
b[3] = fi.CamIndex
b[4] = fi.OnlineNum
+43 -9
View File
@@ -1,16 +1,16 @@
package tutk
import "encoding/binary"
// https://github.com/seydx/tutk_wyze#11-codec-reference
const (
CodecH264 = 0x4e
CodecH265 = 0x50
CodecPCMA = 0x8a
CodecPCML = 0x8c
CodecAAC = 0x88
import (
"encoding/binary"
"time"
)
func GenSessionID() []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
return b
}
func ICAM(cmd uint32, args ...byte) []byte {
// 0 4943414d ICAM
// 4 d807ff00 command
@@ -26,3 +26,37 @@ func ICAM(cmd uint32, args ...byte) []byte {
copy(b[23:], args)
return b
}
func HL(cmdID uint16, payload []byte) []byte {
// 0-1 "HL" magic
// 2 version (typically 5)
// 3 reserved
// 4-5 cmdID command ID (uint16 LE)
// 6-7 payloadLen payload length (uint16 LE)
// 8-15 reserved
// 16+ payload
const headerSize = 16
const version = 5
b := make([]byte, headerSize+len(payload))
copy(b, "HL")
b[2] = version
binary.LittleEndian.PutUint16(b[4:], cmdID)
binary.LittleEndian.PutUint16(b[6:], uint16(len(payload)))
copy(b[headerSize:], payload)
return b
}
func ParseHL(data []byte) (cmdID uint16, payload []byte, ok bool) {
if len(data) < 16 || data[0] != 'H' || data[1] != 'L' {
return 0, nil, false
}
cmdID = binary.LittleEndian.Uint16(data[4:])
payloadLen := binary.LittleEndian.Uint16(data[6:])
if len(data) >= 16+int(payloadLen) {
payload = data[16 : 16+payloadLen]
} else if len(data) > 16 {
payload = data[16:]
}
return cmdID, payload, true
}
-6
View File
@@ -155,9 +155,3 @@ func ConnectByUID(stage byte, uid string, sid8 []byte) []byte {
return b
}
func GenSessionID() []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
return b
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
+50 -23
View File
@@ -12,8 +12,7 @@ import (
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/wyze/crypto"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
)
const (
@@ -29,15 +28,6 @@ const (
BitrateSD uint16 = 0x3C
)
const (
QualityUnknown = 0
QualityMax = 1
QualityHigh = 2
QualityMiddle = 3
QualityLow = 4
QualityMin = 5
)
const (
MediaTypeVideo = 1
MediaTypeAudio = 2
@@ -59,7 +49,7 @@ const (
)
type Client struct {
conn *tutk.Conn
conn *tutk.DTLSConn
host string
uid string
@@ -76,7 +66,7 @@ type Client struct {
hasAudio bool
hasIntercom bool
audioCodecID uint16
audioCodecID byte
audioSampleRate uint32
audioChannels uint8
}
@@ -107,7 +97,7 @@ func Dial(rawURL string) (*Client, error) {
verbose: query.Get("verbose") == "true",
}
c.authKey = string(crypto.CalculateAuthKey(c.enr, c.mac))
c.authKey = string(tutk.CalculateAuthKey(c.enr, c.mac))
if c.verbose {
fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
@@ -143,13 +133,13 @@ func (c *Client) SupportsIntercom() bool {
return c.hasIntercom
}
func (c *Client) SetBackchannelCodec(codecID uint16, sampleRate uint32, channels uint8) {
func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
c.audioCodecID = codecID
c.audioSampleRate = sampleRate
c.audioChannels = channels
}
func (c *Client) GetBackchannelCodec() (codecID uint16, sampleRate uint32, channels uint8) {
func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
return c.audioCodecID, c.audioSampleRate, c.audioChannels
}
@@ -238,13 +228,13 @@ func (c *Client) ReadPacket() (*tutk.Packet, error) {
return c.conn.AVRecvFrameData()
}
func (c *Client) WriteAudio(codec uint16, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
func (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
if !c.conn.IsBackchannelReady() {
return fmt.Errorf("speaker channel not connected")
}
if c.verbose {
fmt.Printf("[Wyze] WriteAudio: codec=0x%04x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
fmt.Printf("[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
}
return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
@@ -305,7 +295,7 @@ func (c *Client) connect() error {
host = host[:idx]
}
conn, err := tutk.Dial(host, port, c.uid, c.authKey, c.enr, c.mac, c.verbose)
conn, err := tutk.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
if err != nil {
return fmt.Errorf("wyze: connect failed: %w", err)
}
@@ -386,9 +376,7 @@ func (c *Client) doKAuth() error {
fmt.Printf("[Wyze] K10003 auth success\n")
}
if avResp := c.conn.GetAVLoginResponse(); avResp != nil {
c.hasIntercom = avResp.TwoWayStreaming == 1
}
c.hasIntercom = c.conn.HasTwoWayStreaming()
if c.verbose {
fmt.Printf("[Wyze] K-auth complete\n")
@@ -409,7 +397,7 @@ func (c *Client) buildK10000() []byte {
}
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
resp := crypto.GenerateChallengeResponse(challenge, c.enr, status)
resp := generateChallengeResponse(challenge, c.enr, status)
sessionID := make([]byte, 4)
rand.Read(sessionID)
b := make([]byte, 38)
@@ -555,3 +543,42 @@ func (c *Client) is2K() bool {
func (c *Client) isFloodlight() bool {
return c.model == "HL_CFL2"
}
const (
statusDefault byte = 1
statusENR16 byte = 3
statusENR32 byte = 6
)
func generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
var secretKey []byte
switch status {
case statusDefault:
secretKey = []byte("FFFFFFFFFFFFFFFF")
case statusENR16:
if len(enr) >= 16 {
secretKey = []byte(enr[:16])
} else {
secretKey = make([]byte, 16)
copy(secretKey, enr)
}
case statusENR32:
if len(enr) >= 16 {
firstKey := []byte(enr[:16])
challengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)
}
if len(enr) >= 32 {
secretKey = []byte(enr[16:32])
} else if len(enr) > 16 {
secretKey = make([]byte, 16)
copy(secretKey, []byte(enr[16:]))
} else {
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
default:
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
return tutk.XXTEADecryptVar(challengeBytes, secretKey)
}
-143
View File
@@ -1,143 +0,0 @@
package crypto
import (
"bytes"
"crypto/rand"
"encoding/binary"
"math/bits"
)
const charlie = "Charlie is the designer of P2P!!"
func TransCodePartial(src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
dst := bytes.Clone(src)
src16, tmp16, dst16 := src, tmp, dst
for ; n >= 16; n -= 16 {
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, -i-1))
}
for i := range 16 {
dst16[i] = tmp16[i] ^ charlie[i]
}
swap(dst16, tmp16, 16)
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, -i-3))
}
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
}
for i := 0; i < n; i++ {
tmp16[i] = src16[i] ^ charlie[i]
}
swap(tmp16, dst16, n)
return dst
}
func ReverseTransCodePartial(src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
dst := bytes.Clone(src)
src16, tmp16, dst16 := src, tmp, dst
for ; n >= 16; n -= 16 {
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3))
}
swap(tmp16, dst16, 16)
for i := range 16 {
tmp16[i] = dst16[i] ^ charlie[i]
}
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, i+1))
}
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
}
swap(src16, tmp16, n)
for i := 0; i < n; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
return dst
}
func TransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return TransCodePartial(src)
}
dst := make([]byte, len(src))
header := TransCodePartial(src[:16])
copy(dst, header)
if len(src) > 16 {
if src[3]&1 != 0 { // Partial encryption
remaining := len(src) - 16
encryptLen := min(remaining, 48)
if encryptLen > 0 {
encrypted := TransCodePartial(src[16 : 16+encryptLen])
copy(dst[16:], encrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full encryption
encrypted := TransCodePartial(src[16:])
copy(dst[16:], encrypted)
}
}
return dst
}
func ReverseTransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return ReverseTransCodePartial(src)
}
dst := make([]byte, len(src))
header := ReverseTransCodePartial(src[:16])
copy(dst, header)
if len(src) > 16 {
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
remaining := len(src) - 16
decryptLen := min(remaining, 48)
if decryptLen > 0 {
decrypted := ReverseTransCodePartial(src[16 : 16+decryptLen])
copy(dst[16:], decrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full decryption
decrypted := ReverseTransCodePartial(src[16:])
copy(dst[16:], decrypted)
}
}
return dst
}
func RandRead(b []byte) {
_, _ = rand.Read(b)
}
func swap(src, dst []byte, n int) {
switch n {
case 8:
dst[0], dst[1], dst[2], dst[3] = src[7], src[4], src[3], src[2]
dst[4], dst[5], dst[6], dst[7] = src[1], src[6], src[5], src[0]
case 16:
dst[0], dst[1], dst[2], dst[3] = src[11], src[9], src[8], src[15]
dst[4], dst[5], dst[6], dst[7] = src[13], src[10], src[12], src[14]
dst[8], dst[9], dst[10], dst[11] = src[2], src[1], src[5], src[0]
dst[12], dst[13], dst[14], dst[15] = src[6], src[4], src[7], src[3]
default:
copy(dst, src[:n])
}
}
-147
View File
@@ -1,147 +0,0 @@
package crypto
import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strings"
)
const delta = 0x9e3779b9
const (
StatusDefault byte = 1
StatusENR16 byte = 3
StatusENR32 byte = 6
)
func XXTEADecrypt(data, key []byte) []byte {
if len(data) < 8 || len(key) < 16 {
return nil
}
k := make([]uint32, 4)
for i := range 4 {
k[i] = binary.LittleEndian.Uint32(key[i*4:])
}
n := max(len(data)/4, 2)
v := make([]uint32, n)
for i := 0; i < len(data)/4; i++ {
v[i] = binary.LittleEndian.Uint32(data[i*4:])
}
rounds := 6 + 52/n
sum := uint32(rounds) * delta
y := v[0]
for rounds > 0 {
e := (sum >> 2) & 3
for p := n - 1; p > 0; p-- {
z := v[p-1]
v[p] -= mx(sum, y, z, p, e, k)
y = v[p]
}
z := v[n-1]
v[0] -= mx(sum, y, z, 0, e, k)
y = v[0]
sum -= delta
rounds--
}
result := make([]byte, n*4)
for i := range n {
binary.LittleEndian.PutUint32(result[i*4:], v[i])
}
return result[:len(data)]
}
func XXTEAEncrypt(data, key []byte) []byte {
if len(data) < 8 || len(key) < 16 {
return nil
}
k := make([]uint32, 4)
for i := range 4 {
k[i] = binary.LittleEndian.Uint32(key[i*4:])
}
n := max(len(data)/4, 2)
v := make([]uint32, n)
for i := 0; i < len(data)/4; i++ {
v[i] = binary.LittleEndian.Uint32(data[i*4:])
}
rounds := 6 + 52/n
var sum uint32
z := v[n-1]
for rounds > 0 {
sum += delta
e := (sum >> 2) & 3
for p := 0; p < n-1; p++ {
y := v[p+1]
v[p] += mx(sum, y, z, p, e, k)
z = v[p]
}
y := v[0]
v[n-1] += mx(sum, y, z, n-1, e, k)
z = v[n-1]
rounds--
}
result := make([]byte, n*4)
for i := range n {
binary.LittleEndian.PutUint32(result[i*4:], v[i])
}
return result[:len(data)]
}
func mx(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
}
func GenerateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
var secretKey []byte
switch status {
case StatusDefault:
secretKey = []byte("FFFFFFFFFFFFFFFF")
case StatusENR16:
if len(enr) >= 16 {
secretKey = []byte(enr[:16])
} else {
secretKey = make([]byte, 16)
copy(secretKey, enr)
}
case StatusENR32:
if len(enr) >= 16 {
firstKey := []byte(enr[:16])
challengeBytes = XXTEADecrypt(challengeBytes, firstKey)
}
if len(enr) >= 32 {
secretKey = []byte(enr[16:32])
} else if len(enr) > 16 {
secretKey = make([]byte, 16)
copy(secretKey, []byte(enr[16:]))
} else {
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
default:
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
return XXTEADecrypt(challengeBytes, secretKey)
}
func CalculateAuthKey(enr, mac string) []byte {
data := enr + strings.ToUpper(mac)
hash := sha256.Sum256([]byte(data))
b64 := base64.StdEncoding.EncodeToString(hash[:6])
b64 = strings.ReplaceAll(b64, "+", "Z")
b64 = strings.ReplaceAll(b64, "/", "9")
b64 = strings.ReplaceAll(b64, "=", "A")
return []byte(b64)
}
+14 -14
View File
@@ -10,7 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
@@ -96,21 +96,21 @@ func (p *Producer) Start() error {
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case tutk.AudioCodecG711U:
case tutk.CodecPCMU:
name = core.CodecPCMU
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecG711A:
case tutk.CodecPCMA:
name = core.CodecPCMA
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecAACADTS, tutk.AudioCodecAACWyze, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
case tutk.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:
name = core.CodecAAC
payload := pkt.Payload
if aac.IsADTS(payload) {
@@ -121,21 +121,21 @@ func (p *Producer) Start() error {
Payload: payload,
}
case tutk.AudioCodecOpus:
case tutk.CodecOpus:
name = core.CodecOpus
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecPCM:
case tutk.CodecPCML:
name = core.CodecPCML
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecMP3:
case tutk.CodecMP3:
name = core.CodecMP3
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
@@ -167,7 +167,7 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
client.SetDeadline(time.Now().Add(core.ProbeTimeout))
var vcodec, acodec *core.Codec
var tutkAudioCodec uint16
var tutkAudioCodec byte
for {
if client.verbose {
@@ -197,33 +197,33 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
vcodec = h265.AVCCToCodec(buf)
}
}
case tutk.AudioCodecG711U:
case tutk.CodecPCMU:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecG711A:
case tutk.CodecPCMA:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecAACWyze, tutk.AudioCodecAACADTS, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
case tutk.CodecAACAlt, tutk.CodecAACADTS, tutk.CodecAACRaw, tutk.CodecAACLATM:
if acodec == nil {
config := aac.EncodeConfig(aac.TypeAACLC, pkt.SampleRate, pkt.Channels, false)
acodec = aac.ConfigToCodec(config)
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecOpus:
case tutk.CodecOpus:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecPCM:
case tutk.CodecPCML:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecMP3:
case tutk.CodecMP3:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
File diff suppressed because it is too large Load Diff
-281
View File
@@ -1,281 +0,0 @@
package tutk
type AVLoginResponse struct {
ServerType uint32
Resend int32
TwoWayStreaming int32
SyncRecvData int32
SecurityMode uint32
VideoOnConnect int32
AudioOnConnect int32
}
const (
CodecUnknown uint16 = 0x00
CodecMPEG4 uint16 = 0x4C // 76
CodecH263 uint16 = 0x4D // 77
CodecH264 uint16 = 0x4E // 78
CodecMJPEG uint16 = 0x4F // 79
CodecH265 uint16 = 0x50 // 80
)
const (
AudioCodecAACRaw uint16 = 0x86 // 134
AudioCodecAACADTS uint16 = 0x87 // 135
AudioCodecAACLATM uint16 = 0x88 // 136
AudioCodecG711U uint16 = 0x89 // 137
AudioCodecG711A uint16 = 0x8A // 138
AudioCodecADPCM uint16 = 0x8B // 139
AudioCodecPCM uint16 = 0x8C // 140
AudioCodecSPEEX uint16 = 0x8D // 141
AudioCodecMP3 uint16 = 0x8E // 142
AudioCodecG726 uint16 = 0x8F // 143
AudioCodecAACWyze uint16 = 0x90 // 144
AudioCodecOpus uint16 = 0x92 // 146
)
const (
SampleRate8K uint8 = 0x00
SampleRate11K uint8 = 0x01
SampleRate12K uint8 = 0x02
SampleRate16K uint8 = 0x03
SampleRate22K uint8 = 0x04
SampleRate24K uint8 = 0x05
SampleRate32K uint8 = 0x06
SampleRate44K uint8 = 0x07
SampleRate48K uint8 = 0x08
)
var sampleRates = map[uint8]int{
SampleRate8K: 8000,
SampleRate11K: 11025,
SampleRate12K: 12000,
SampleRate16K: 16000,
SampleRate22K: 22050,
SampleRate24K: 24000,
SampleRate32K: 32000,
SampleRate44K: 44100,
SampleRate48K: 48000,
}
var samplesPerFrame = map[uint16]uint32{
AudioCodecAACRaw: 1024,
AudioCodecAACADTS: 1024,
AudioCodecAACLATM: 1024,
AudioCodecAACWyze: 1024,
AudioCodecG711U: 160,
AudioCodecG711A: 160,
AudioCodecPCM: 160,
AudioCodecADPCM: 160,
AudioCodecSPEEX: 160,
AudioCodecMP3: 1152,
AudioCodecG726: 160,
AudioCodecOpus: 960,
}
const (
IOTypeVideoStart = 0x01FF
IOTypeVideoStop = 0x02FF
IOTypeAudioStart = 0x0300
IOTypeAudioStop = 0x0301
IOTypeSpeakerStart = 0x0350
IOTypeSpeakerStop = 0x0351
IOTypeGetAudioOutFormatReq = 0x032A
IOTypeGetAudioOutFormatRes = 0x032B
IOTypeSetStreamCtrlReq = 0x0320
IOTypeSetStreamCtrlRes = 0x0321
IOTypeGetStreamCtrlReq = 0x0322
IOTypeGetStreamCtrlRes = 0x0323
IOTypeDevInfoReq = 0x0340
IOTypeDevInfoRes = 0x0341
IOTypeGetSupportStreamReq = 0x0344
IOTypeGetSupportStreamRes = 0x0345
IOTypeSetRecordReq = 0x0310
IOTypeSetRecordRes = 0x0311
IOTypeGetRecordReq = 0x0312
IOTypeGetRecordRes = 0x0313
IOTypePTZCommand = 0x1001
IOTypeReceiveFirstFrame = 0x1002
IOTypeGetEnvironmentReq = 0x030A
IOTypeGetEnvironmentRes = 0x030B
IOTypeSetVideoModeReq = 0x030C
IOTypeSetVideoModeRes = 0x030D
IOTypeGetVideoModeReq = 0x030E
IOTypeGetVideoModeRes = 0x030F
IOTypeSetTimeReq = 0x0316
IOTypeSetTimeRes = 0x0317
IOTypeGetTimeReq = 0x0318
IOTypeGetTimeRes = 0x0319
IOTypeSetWifiReq = 0x0102
IOTypeSetWifiRes = 0x0103
IOTypeGetWifiReq = 0x0104
IOTypeGetWifiRes = 0x0105
IOTypeListWifiAPReq = 0x0106
IOTypeListWifiAPRes = 0x0107
IOTypeSetMotionDetectReq = 0x0306
IOTypeSetMotionDetectRes = 0x0307
IOTypeGetMotionDetectReq = 0x0308
IOTypeGetMotionDetectRes = 0x0309
)
// OLD Protocol (IOTC/TransCode)
const (
CmdDiscoReq uint16 = 0x0601
CmdDiscoRes uint16 = 0x0602
CmdSessionReq uint16 = 0x0402
CmdSessionRes uint16 = 0x0404
CmdDataTX uint16 = 0x0407
CmdDataRX uint16 = 0x0408
CmdKeepaliveReq uint16 = 0x0427
CmdKeepaliveRes uint16 = 0x0428
OldHeaderSize = 16
OldDiscoBodySize = 72
OldDiscoSize = OldHeaderSize + OldDiscoBodySize
OldSessionBody = 36
OldSessionSize = OldHeaderSize + OldSessionBody
)
// NEW Protocol (0xCC51)
const (
MagicNewProto uint16 = 0xCC51
CmdNewDisco uint16 = 0x1002
CmdNewKeepalive uint16 = 0x1202
CmdNewClose uint16 = 0x1302
CmdNewDTLS uint16 = 0x1502
NewPayloadSize uint16 = 0x0028
NewPacketSize = 52
NewHeaderSize = 28
NewAuthSize = 20
NewKeepaliveSize = 48
)
const (
UIDSize = 20
RandIDSize = 8
)
const (
MagicAVLoginResp uint16 = 0x2100
MagicIOCtrl uint16 = 0x7000
MagicChannelMsg uint16 = 0x1000
MagicACK uint16 = 0x0009
MagicAVLogin1 uint16 = 0x0000
MagicAVLogin2 uint16 = 0x2000
)
const (
ProtoVersion uint16 = 0x000c
DefaultCaps uint32 = 0x001f07fb
)
const (
IOTCChannelMain = 0 // Main AV (we = DTLS Client)
IOTCChannelBack = 1 // Backchannel (we = DTLS Server)
)
const (
PSKIdentity = "AUTHPWD_admin"
DefaultUser = "admin"
DefaultPort = 32761
)
func CodecName(id uint16) string {
switch id {
case CodecH264:
return "H264"
case CodecH265:
return "H265"
case CodecMPEG4:
return "MPEG4"
case CodecH263:
return "H263"
case CodecMJPEG:
return "MJPEG"
default:
return "Unknown"
}
}
func AudioCodecName(id uint16) string {
switch id {
case AudioCodecG711U:
return "PCMU"
case AudioCodecG711A:
return "PCMA"
case AudioCodecPCM:
return "PCM"
case AudioCodecAACLATM, AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACWyze:
return "AAC"
case AudioCodecOpus:
return "Opus"
case AudioCodecSPEEX:
return "Speex"
case AudioCodecMP3:
return "MP3"
case AudioCodecG726:
return "G726"
case AudioCodecADPCM:
return "ADPCM"
default:
return "Unknown"
}
}
func SampleRateValue(idx uint8) int {
if rate, ok := sampleRates[idx]; ok {
return rate
}
return 16000
}
func SampleRateIndex(hz uint32) uint8 {
switch hz {
case 8000:
return SampleRate8K
case 11025:
return SampleRate11K
case 12000:
return SampleRate12K
case 16000:
return SampleRate16K
case 22050:
return SampleRate22K
case 24000:
return SampleRate24K
case 32000:
return SampleRate32K
case 44100:
return SampleRate44K
case 48000:
return SampleRate48K
default:
return SampleRate16K
}
}
func BuildAudioFlags(sampleRate uint32, bits16, stereo bool) uint8 {
flags := SampleRateIndex(sampleRate) << 2
if bits16 {
flags |= 0x02
}
if stereo {
flags |= 0x01
}
return flags
}
func IsVideoCodec(id uint16) bool {
return id >= CodecMPEG4 && id <= CodecH265
}
func IsAudioCodec(id uint16) bool {
return id >= AudioCodecAACRaw && id <= AudioCodecOpus
}
func GetSamplesPerFrame(codecID uint16) uint32 {
if samples, ok := samplesPerFrame[codecID]; ok {
return samples
}
return 1024
}
+1 -1
View File
@@ -107,7 +107,7 @@ func (c *Client) ReadPacket() (hdr, payload []byte, err error) {
switch hdr[0] {
case tutk.CodecH264, tutk.CodecH265:
payload, err = DecodeVideo(payload, c.key)
case tutk.CodecAAC:
case tutk.CodecAACLATM:
payload, err = crypto.Decode(payload, c.key)
}
}
+2 -2
View File
@@ -98,7 +98,7 @@ func probe(client *Client) ([]*core.Media, error) {
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: 8000}
}
case tutk.CodecAAC:
case tutk.CodecAACLATM:
if acodec == nil {
acodec = aac.ADTSToCodec(payload)
if acodec != nil {
@@ -187,7 +187,7 @@ func (c *Producer) Start() error {
audioTS += uint32(n / 2) // because 16bit
}
case tutk.CodecAAC:
case tutk.CodecAACLATM:
pkt = &core.Packet{
Header: rtp.Header{
SequenceNumber: audioSeq,