From 60250a32c2e992ba9bae72fa2dc8c50e449ac51a Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 12 Mar 2025 22:28:30 +0300 Subject: [PATCH] Fix support HKSV for HomeKit cameras #684 --- examples/homekit_info/main.go | 2 +- pkg/hap/camera/ch131_data_stream.go | 17 +++ pkg/hap/hds/hds.go | 123 ++++++++++++++++ pkg/hap/hds/hds_test.go | 35 +++++ pkg/hap/helpers.go | 9 +- pkg/hap/secure/secure.go | 5 +- pkg/homekit/proxy.go | 215 +++++++++++++++++++++++----- 7 files changed, 364 insertions(+), 42 deletions(-) create mode 100644 pkg/hap/camera/ch131_data_stream.go create mode 100644 pkg/hap/hds/hds.go create mode 100644 pkg/hap/hds/hds_test.go diff --git a/examples/homekit_info/main.go b/examples/homekit_info/main.go index d353ae79..8527042e 100644 --- a/examples/homekit_info/main.go +++ b/examples/homekit_info/main.go @@ -54,7 +54,7 @@ var chars = map[string]string{ "21C": "Third Party Camera Active", "21D": "Camera Operating Mode Indicator", "11B": "Night Vision", - "129": "Supported Data Stream Transport Configuration", + //"129": "Supported Data Stream Transport Configuration", "37": "Version", "131": "Setup Data Stream Transport", "130": "Supported Data Stream Transport Configuration", diff --git a/pkg/hap/camera/ch131_data_stream.go b/pkg/hap/camera/ch131_data_stream.go new file mode 100644 index 00000000..067b01b4 --- /dev/null +++ b/pkg/hap/camera/ch131_data_stream.go @@ -0,0 +1,17 @@ +package camera + +const TypeSetupDataStreamTransport = "131" + +type SetupDataStreamRequest struct { + SessionCommandType byte `tlv8:"1"` + TransportType byte `tlv8:"2"` + ControllerKeySalt string `tlv8:"3"` +} + +type SetupDataStreamResponse struct { + Status byte `tlv8:"1"` + TransportTypeSessionParameters struct { + TCPListeningPort uint16 `tlv8:"1"` + } `tlv8:"2"` + AccessoryKeySalt string `tlv8:"3"` +} diff --git a/pkg/hap/hds/hds.go b/pkg/hap/hds/hds.go new file mode 100644 index 00000000..a7b2c74a --- /dev/null +++ b/pkg/hap/hds/hds.go @@ -0,0 +1,123 @@ +// Package hds - HomeKit Data Stream +package hds + +import ( + "bufio" + "encoding/binary" + "io" + "net" + "time" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" + "github.com/AlexxIT/go2rtc/pkg/hap/secure" +) + +func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { + writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key") + if err != nil { + return nil, err + } + + readKey, err := hkdf.Sha512(key, salt, "HDS-Read-Encryption-Key") + if err != nil { + return nil, err + } + + c := &Conn{ + conn: conn, + rd: bufio.NewReaderSize(conn, 32*1024), + wr: bufio.NewWriterSize(conn, 32*1024), + } + + if controller { + c.decryptKey, c.encryptKey = readKey, writeKey + } else { + c.decryptKey, c.encryptKey = writeKey, readKey + } + + return c, nil +} + +type Conn struct { + conn net.Conn + + rd *bufio.Reader + wr *bufio.Writer + + decryptKey []byte + encryptKey []byte + decryptCnt uint64 + encryptCnt uint64 +} + +func (c *Conn) Read(p []byte) (n int, err error) { + verify := make([]byte, 4) + if _, err = io.ReadFull(c.rd, verify); err != nil { + return + } + + n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF) + + ciphertext := make([]byte, n+secure.Overhead) + if _, err = io.ReadFull(c.rd, ciphertext); err != nil { + return + } + + nonce := make([]byte, secure.NonceSize) + binary.LittleEndian.PutUint64(nonce, c.decryptCnt) + c.decryptCnt++ + + _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify) + return +} + +func (c *Conn) Write(b []byte) (n int, err error) { + n = len(b) + + verify := make([]byte, 4) + binary.BigEndian.PutUint32(verify, 0x01000000|uint32(n)) + if _, err = c.wr.Write(verify); err != nil { + return + } + + nonce := make([]byte, secure.NonceSize) + binary.LittleEndian.PutUint64(nonce, c.encryptCnt) + c.encryptCnt++ + + buf := make([]byte, n+secure.Overhead) + if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil { + return + } + + if _, err = c.wr.Write(buf); err != nil { + return + } + + err = c.wr.Flush() + return +} + +func (c *Conn) Close() error { + return c.conn.Close() +} + +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *Conn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/pkg/hap/hds/hds_test.go b/pkg/hap/hds/hds_test.go new file mode 100644 index 00000000..f1c85455 --- /dev/null +++ b/pkg/hap/hds/hds_test.go @@ -0,0 +1,35 @@ +package hds + +import ( + "bufio" + "bytes" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/stretchr/testify/require" +) + +func TestEncryption(t *testing.T) { + key := []byte(core.RandString(16, 0)) + salt := core.RandString(32, 0) + + c, err := Client(nil, key, salt, true) + require.NoError(t, err) + + buf := bytes.NewBuffer(nil) + c.wr = bufio.NewWriter(buf) + + n, err := c.Write([]byte("test")) + require.NoError(t, err) + require.Equal(t, 4, n) + + c, err = Client(nil, key, salt, false) + c.rd = bufio.NewReader(buf) + require.NoError(t, err) + + b := make([]byte, 32) + n, err = c.Read(b) + require.NoError(t, err) + + require.Equal(t, "test", string(b[:n])) +} diff --git a/pkg/hap/helpers.go b/pkg/hap/helpers.go index ea5e4059..d1400b84 100644 --- a/pkg/hap/helpers.go +++ b/pkg/hap/helpers.go @@ -64,10 +64,11 @@ type JSONCharacters struct { } type JSONCharacter struct { - AID uint8 `json:"aid"` - IID uint64 `json:"iid"` - Value any `json:"value,omitempty"` - Event any `json:"ev,omitempty"` + AID uint8 `json:"aid"` + IID uint64 `json:"iid"` + Status any `json:"status,omitempty"` + Value any `json:"value,omitempty"` + Event any `json:"ev,omitempty"` } func SanitizePin(pin string) (string, error) { diff --git a/pkg/hap/secure/secure.go b/pkg/hap/secure/secure.go index 0c33b356..576ee127 100644 --- a/pkg/hap/secure/secure.go +++ b/pkg/hap/secure/secure.go @@ -6,7 +6,6 @@ import ( "errors" "io" "net" - "sync" "time" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" @@ -24,7 +23,7 @@ type Conn struct { encryptCnt uint64 decryptCnt uint64 - mx sync.Mutex + SharedKey []byte } func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { @@ -42,6 +41,8 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { conn: conn, rd: bufio.NewReaderSize(conn, 32*1024), wr: bufio.NewWriterSize(conn, 32*1024), + + SharedKey: sharedKey, } if isClient { diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index 77170fe2..be233042 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -3,65 +3,210 @@ package homekit import ( "bufio" "bytes" + "encoding/json" + "fmt" + "io" "net" "net/http" "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/hap/hds" + "github.com/AlexxIT/go2rtc/pkg/hap/secure" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc { - return func(controller net.Conn) error { - accessory, err := dial() + return func(con net.Conn) error { + defer con.Close() + + acc, err := dial() + if err != nil { + return err + } + defer acc.Close() + + pr := &Proxy{ + con: con.(*secure.Conn), + acc: acc.(*secure.Conn), + res: make(chan *http.Response), + } + + // accessory (ex. Camera) => controller (ex. iPhone) + go pr.handleAcc() + + // controller => accessory + return pr.handleCon(pair) + } +} + +type Proxy struct { + con *secure.Conn + acc *secure.Conn + res chan *http.Response +} + +func (p *Proxy) handleCon(pair ServerPair) error { + var hdsCharIID uint64 + + rd := bufio.NewReader(p.con) + for { + req, err := http.ReadRequest(rd) if err != nil { return err } - // accessory (ex. Camera) => controller (ex. iPhone) - go proxy(accessory, controller, nil) + var hdsConSalt string - // controller => accessory - return proxy(controller, accessory, pair) + switch { + case req.Method == "POST" && req.URL.Path == hap.PathPairings: + var res *http.Response + if res, err = handlePairings(p.con, req, pair); err != nil { + return err + } + if err = res.Write(p.con); err != nil { + return err + } + continue + case req.Method == "PUT" && req.URL.Path == hap.PathCharacteristics && hdsCharIID != 0: + body, _ := io.ReadAll(req.Body) + var v hap.JSONCharacters + _ = json.Unmarshal(body, &v) + for _, char := range v.Value { + if char.IID == hdsCharIID { + var hdsReq camera.SetupDataStreamRequest + _ = tlv8.UnmarshalBase64(char.Value, &hdsReq) + hdsConSalt = hdsReq.ControllerKeySalt + break + } + } + req.Body = io.NopCloser(bytes.NewReader(body)) + } + + if err = req.Write(p.acc); err != nil { + return err + } + + res := <-p.res + + switch { + case req.Method == "GET" && req.URL.Path == hap.PathAccessories: + body, _ := io.ReadAll(res.Body) + var v hap.JSONAccessories + if err = json.Unmarshal(body, &v); err != nil { + return err + } + for _, acc := range v.Value { + if char := acc.GetCharacter(camera.TypeSetupDataStreamTransport); char != nil { + hdsCharIID = char.IID + } + break + } + res.Body = io.NopCloser(bytes.NewReader(body)) + + case hdsConSalt != "": + body, _ := io.ReadAll(res.Body) + var v hap.JSONCharacters + _ = json.Unmarshal(body, &v) + for i, char := range v.Value { + if char.IID == hdsCharIID { + var hdsRes camera.SetupDataStreamResponse + _ = tlv8.UnmarshalBase64(char.Value, &hdsRes) + + hdsAccSalt := hdsRes.AccessoryKeySalt + hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort) + + // swtich accPort to conPort + hdsPort, err = p.listenHDS(hdsPort, hdsConSalt+hdsAccSalt) + if err != nil { + return err + } + + hdsRes.TransportTypeSessionParameters.TCPListeningPort = uint16(hdsPort) + if v.Value[i].Value, err = tlv8.MarshalBase64(hdsRes); err != nil { + return err + } + body, _ = json.Marshal(v) + res.ContentLength = int64(len(body)) + break + } + } + res.Body = io.NopCloser(bytes.NewReader(body)) + } + + if err = res.Write(p.con); err != nil { + return err + } } } -func proxy(r, w net.Conn, pair ServerPair) error { - b := make([]byte, 64*1024) +func (p *Proxy) handleAcc() error { + rd := bufio.NewReader(p.acc) for { - n, err := r.Read(b) + res, err := hap.ReadResponse(rd, nil) if err != nil { - break + return err } - if pair != nil && bytes.HasPrefix(b[:n], []byte("POST /pairings HTTP/1.1")) { - buf := bytes.NewBuffer(b[:n]) - req, err := http.ReadRequest(bufio.NewReader(buf)) - if err != nil { - return err - } - - res, err := handlePairings(r, req, pair) - if err != nil { - return err - } - - buf.Reset() - - if err = res.Write(buf); err != nil { - return err - } - if _, err = buf.WriteTo(r); err != nil { + if res.Proto == hap.ProtoEvent { + if err = res.Write(p.con); err != nil { return err } continue } - //log.Printf("[hap] %d bytes => %s\n%.512s", n, w.RemoteAddr(), b[:n]) - - if _, err = w.Write(b[:n]); err != nil { - break + // important to read body before next read response + body, err := io.ReadAll(res.Body) + if err != nil { + return err } + res.Body = io.NopCloser(bytes.NewReader(body)) + + p.res <- res } - _ = r.Close() - _ = w.Close() - return nil +} + +func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { + ln, err := net.ListenTCP("tcp", nil) + if err != nil { + return 0, err + } + + go func() { + defer ln.Close() + + // raw controller conn + con, err := ln.Accept() + if err != nil { + return + } + defer con.Close() + + // secured controller conn (controlle=false because we are accessory) + con, err = hds.Client(con, p.con.SharedKey, salt, false) + if err != nil { + return + } + + accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP + + // raw accessory conn + acc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", accIP, accPort)) + if err != nil { + return + } + defer acc.Close() + + // secured accessory conn (controller=true because we are controller) + acc, err = hds.Client(acc, p.acc.SharedKey, salt, true) + if err != nil { + return + } + + go io.Copy(con, acc) + _, _ = io.Copy(acc, con) + }() + + conPort := ln.Addr().(*net.TCPAddr).Port + return conPort, nil }