mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-07 01:03:07 +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",
|
||||
"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",
|
||||
|
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 {
|
||||
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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user