Fix support HKSV for HomeKit cameras #684

This commit is contained in:
Alex X
2025-03-12 22:28:30 +03:00
parent 6a4c73db03
commit 60250a32c2
7 changed files with 364 additions and 42 deletions

View File

@@ -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",

View File

@@ -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"`
}

123
pkg/hap/hds/hds.go Normal file
View File

@@ -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)
}

35
pkg/hap/hds/hds_test.go Normal file
View File

@@ -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]))
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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
}