mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-07 09:11:28 +08:00
Fix support HKSV for HomeKit cameras #684
This commit is contained in:
@@ -54,7 +54,7 @@ var chars = map[string]string{
|
|||||||
"21C": "Third Party Camera Active",
|
"21C": "Third Party Camera Active",
|
||||||
"21D": "Camera Operating Mode Indicator",
|
"21D": "Camera Operating Mode Indicator",
|
||||||
"11B": "Night Vision",
|
"11B": "Night Vision",
|
||||||
"129": "Supported Data Stream Transport Configuration",
|
//"129": "Supported Data Stream Transport Configuration",
|
||||||
"37": "Version",
|
"37": "Version",
|
||||||
"131": "Setup Data Stream Transport",
|
"131": "Setup Data Stream Transport",
|
||||||
"130": "Supported Data Stream Transport Configuration",
|
"130": "Supported Data Stream Transport Configuration",
|
||||||
|
17
pkg/hap/camera/ch131_data_stream.go
Normal file
17
pkg/hap/camera/ch131_data_stream.go
Normal 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
123
pkg/hap/hds/hds.go
Normal 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
35
pkg/hap/hds/hds_test.go
Normal 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]))
|
||||||
|
}
|
@@ -64,10 +64,11 @@ type JSONCharacters struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JSONCharacter struct {
|
type JSONCharacter struct {
|
||||||
AID uint8 `json:"aid"`
|
AID uint8 `json:"aid"`
|
||||||
IID uint64 `json:"iid"`
|
IID uint64 `json:"iid"`
|
||||||
Value any `json:"value,omitempty"`
|
Status any `json:"status,omitempty"`
|
||||||
Event any `json:"ev,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
|
Event any `json:"ev,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SanitizePin(pin string) (string, error) {
|
func SanitizePin(pin string) (string, error) {
|
||||||
|
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||||
@@ -24,7 +23,7 @@ type Conn struct {
|
|||||||
encryptCnt uint64
|
encryptCnt uint64
|
||||||
decryptCnt uint64
|
decryptCnt uint64
|
||||||
|
|
||||||
mx sync.Mutex
|
SharedKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
|
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,
|
conn: conn,
|
||||||
rd: bufio.NewReaderSize(conn, 32*1024),
|
rd: bufio.NewReaderSize(conn, 32*1024),
|
||||||
wr: bufio.NewWriterSize(conn, 32*1024),
|
wr: bufio.NewWriterSize(conn, 32*1024),
|
||||||
|
|
||||||
|
SharedKey: sharedKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isClient {
|
if isClient {
|
||||||
|
@@ -3,65 +3,210 @@ package homekit
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
"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 {
|
func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc {
|
||||||
return func(controller net.Conn) error {
|
return func(con net.Conn) error {
|
||||||
accessory, err := dial()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// accessory (ex. Camera) => controller (ex. iPhone)
|
var hdsConSalt string
|
||||||
go proxy(accessory, controller, nil)
|
|
||||||
|
|
||||||
// controller => accessory
|
switch {
|
||||||
return proxy(controller, accessory, pair)
|
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 {
|
func (p *Proxy) handleAcc() error {
|
||||||
b := make([]byte, 64*1024)
|
rd := bufio.NewReader(p.acc)
|
||||||
for {
|
for {
|
||||||
n, err := r.Read(b)
|
res, err := hap.ReadResponse(rd, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if pair != nil && bytes.HasPrefix(b[:n], []byte("POST /pairings HTTP/1.1")) {
|
if res.Proto == hap.ProtoEvent {
|
||||||
buf := bytes.NewBuffer(b[:n])
|
if err = res.Write(p.con); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("[hap] %d bytes => %s\n%.512s", n, w.RemoteAddr(), b[:n])
|
// important to read body before next read response
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
if _, err = w.Write(b[:n]); err != nil {
|
if err != nil {
|
||||||
break
|
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
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user