From 6d82b1ce892ac0eead4d635a40e19721ab35441c Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 23 Jul 2023 22:22:36 +0300 Subject: [PATCH] Total rework HAP pkg and HomeKit source --- go.mod | 15 +- go.sum | 24 - internal/homekit/api.go | 27 +- pkg/hap/README.md | 12 + pkg/hap/camera/ch114_supported_video.go | 44 ++ pkg/hap/camera/ch115_supported_audio.go | 37 + pkg/hap/camera/ch117_selected_stream.go | 52 ++ pkg/hap/camera/ch118_setup_endpoints.go | 33 + pkg/hap/camera/ch120_streaming_status.go | 13 + pkg/hap/camera/client.go | 41 +- pkg/hap/camera/session.go | 62 +- pkg/hap/chacha20poly1305/chacha20poly1305.go | 51 ++ pkg/hap/character.go | 37 +- pkg/hap/client.go | 328 ++++++++ pkg/hap/client_http.go | 60 ++ pkg/hap/client_pairing.go | 376 ++++++++++ pkg/hap/conn.go | 746 ------------------- pkg/hap/curve25519/curve25519.go | 18 + pkg/hap/ed25519/ed25519.go | 24 + pkg/hap/event_reader.go | 68 ++ pkg/hap/helpers.go | 97 ++- pkg/hap/hkdf/hkdf.go | 17 + pkg/hap/http.go | 246 ------ pkg/hap/pairing.go | 410 ---------- pkg/hap/secure.go | 137 ---- pkg/hap/secure/secure.go | 156 ++++ pkg/hap/server.go | 155 ---- pkg/hap/tlv8/tlv8.go | 333 +++++++++ pkg/hap/tlv8/tlv8_test.go | 38 + pkg/homekit/client.go | 406 ++++++---- scripts/README.md | 26 + 31 files changed, 2074 insertions(+), 2015 deletions(-) create mode 100644 pkg/hap/camera/ch114_supported_video.go create mode 100644 pkg/hap/camera/ch115_supported_audio.go create mode 100644 pkg/hap/camera/ch117_selected_stream.go create mode 100644 pkg/hap/camera/ch118_setup_endpoints.go create mode 100644 pkg/hap/camera/ch120_streaming_status.go create mode 100644 pkg/hap/chacha20poly1305/chacha20poly1305.go create mode 100644 pkg/hap/client.go create mode 100644 pkg/hap/client_http.go create mode 100644 pkg/hap/client_pairing.go delete mode 100644 pkg/hap/conn.go create mode 100644 pkg/hap/curve25519/curve25519.go create mode 100644 pkg/hap/ed25519/ed25519.go create mode 100644 pkg/hap/event_reader.go create mode 100644 pkg/hap/hkdf/hkdf.go delete mode 100644 pkg/hap/http.go delete mode 100644 pkg/hap/pairing.go delete mode 100644 pkg/hap/secure.go create mode 100644 pkg/hap/secure/secure.go delete mode 100644 pkg/hap/server.go create mode 100644 pkg/hap/tlv8/tlv8.go create mode 100644 pkg/hap/tlv8/tlv8_test.go diff --git a/go.mod b/go.mod index a6474110..82f66f9f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/AlexxIT/go2rtc go 1.20 require ( - github.com/brutella/hap v0.0.17 github.com/deepch/vdk v0.0.19 github.com/gorilla/websocket v1.5.0 github.com/miekg/dns v1.1.55 @@ -20,13 +19,12 @@ require ( github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.8.4 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 + golang.org/x/crypto v0.11.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/brutella/dnssd v1.2.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-chi/chi v1.5.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -40,18 +38,11 @@ require ( github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/turn/v2 v2.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect - golang.org/x/crypto v0.11.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.11.0 // indirect ) -replace ( - // RTP tlv8 fix - github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 - // fix reading AAC config bytes - github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 -) +// fix reading AAC config bytes +replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 diff --git a/go.sum b/go.sum index c7eba58f..886fd13d 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,11 @@ -github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI= -github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4= github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0= github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg= -github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs= -github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs= -github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= -github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -46,7 +39,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -110,7 +102,6 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -121,22 +112,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= -github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s= -github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= @@ -147,10 +133,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= @@ -164,7 +147,6 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= @@ -178,9 +160,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -216,12 +196,9 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= @@ -245,7 +222,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/homekit/api.go b/internal/homekit/api.go index 8e62d154..ee14ef4a 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -1,14 +1,15 @@ package homekit import ( + "net/http" + "net/url" + "strings" + "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app/store" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/mdns" - "net/http" - "net/url" - "strings" ) func apiHandler(w http.ResponseWriter, r *http.Request) { @@ -32,13 +33,15 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { } err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { + log.Trace().Msgf("[homekit] mdns=%s", entry) + if entry.Complete() { device := Device{ Name: entry.Name, Addr: entry.Addr(), - ID: entry.Info["id"], - Model: entry.Info["md"], - Paired: entry.Info["sf"] == "0", + ID: entry.Info[hap.TXTDeviceID], + Model: entry.Info[hap.TXTModel], + Paired: entry.Info[hap.TXTStatusFlags] == "0", } items = append(items, device) } @@ -73,7 +76,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { } func hkPair(deviceID, pin, name string) (err error) { - var conn *hap.Conn + var conn *hap.Client if conn, err = hap.Pair(deviceID, pin); err != nil { return @@ -94,9 +97,9 @@ func hkDelete(name string) (err error) { continue } - var conn *hap.Conn + var conn *hap.Client - if conn, err = hap.NewConn(rawURL.(string)); err != nil { + if conn, err = hap.NewClient(rawURL.(string)); err != nil { return } @@ -104,12 +107,6 @@ func hkDelete(name string) (err error) { return } - go func() { - if err = conn.Handle(); err != nil { - log.Warn().Err(err).Caller().Send() - } - }() - if err = conn.ListPairings(); err != nil { return } diff --git a/pkg/hap/README.md b/pkg/hap/README.md index 94c4bfd8..a3a1a4f0 100644 --- a/pkg/hap/README.md +++ b/pkg/hap/README.md @@ -27,7 +27,19 @@ - ServerID - same as DeviceID (using for Client auth) - ServerPublic/ServerPrivate - static random 32 byte keypair +## AAC ELD + +Requires ffmpeg built with `--enable-libfdk-aac` + +``` +-acodec libfdk_aac -aprofile aac_eld +``` + ## Useful links +- https://github.com/apple/HomeKitADK/blob/master/Documentation/crypto.md +- https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c - [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys) - [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py) +- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification) +- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/) \ No newline at end of file diff --git a/pkg/hap/camera/ch114_supported_video.go b/pkg/hap/camera/ch114_supported_video.go new file mode 100644 index 00000000..0e9ef66f --- /dev/null +++ b/pkg/hap/camera/ch114_supported_video.go @@ -0,0 +1,44 @@ +package camera + +const TypeSupportedVideoStreamConfiguration = "114" + +type SupportedVideoStreamConfig struct { + Codecs []VideoCodecConfig `tlv8:"1"` +} + +type VideoCodecConfig struct { + CodecType byte `tlv8:"1"` + CodecParams []VideoCodecParams `tlv8:"2"` + VideoAttrs []VideoAttrs `tlv8:"3"` +} + +const ( + VideoCodecTypeH264 = 0 + + VideoCodecProfileConstrainedBaseline = 0 + VideoCodecProfileMain = 1 + VideoCodecProfileHigh = 2 + + VideoCodecLevel31 = 0 + VideoCodecLevel32 = 1 + VideoCodecLevel40 = 2 + + VideoCodecPacketizationModeNonInterleaved = 0 + + VideoCodecCvoNotSuppported = 0 + VideoCodecCvoSuppported = 1 +) + +type VideoCodecParams struct { + ProfileID byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high + Level byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0 + PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved + CVOEnabled byte `tlv8:"4"` // 0 - not supported, 1 - supported + CVOID byte `tlv8:"5"` // ??? +} + +type VideoAttrs struct { + Width uint16 `tlv8:"1"` + Height uint16 `tlv8:"2"` + Framerate uint8 `tlv8:"3"` +} diff --git a/pkg/hap/camera/ch115_supported_audio.go b/pkg/hap/camera/ch115_supported_audio.go new file mode 100644 index 00000000..c9aa8c31 --- /dev/null +++ b/pkg/hap/camera/ch115_supported_audio.go @@ -0,0 +1,37 @@ +package camera + +const TypeSupportedAudioStreamConfiguration = "115" + +type SupportedAudioStreamConfig struct { + Codecs []AudioCodecConfig `tlv8:"1"` + ComfortNoise byte `tlv8:"2"` +} + +const ( + AudioCodecTypePCMU = 0 + AudioCodecTypePCMA = 1 + AudioCodecTypeAACELD = 2 + AudioCodecTypeOpus = 3 + AudioCodecTypeMSBC = 4 + AudioCodecTypeAMR = 5 + AudioCodecTypeARMWB = 6 + + AudioCodecBitrateVariable = 0 + AudioCodecBitrateConstant = 1 + + AudioCodecSampleRate8Khz = 0 + AudioCodecSampleRate16Khz = 1 + AudioCodecSampleRate24Khz = 2 +) + +type AudioCodecConfig struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioCodecParams `tlv8:"2"` +} + +type AudioCodecParams struct { + Channels byte `tlv8:"1"` + Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant + SampleRate byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 + RTPTime byte `tlv8:"4"` +} diff --git a/pkg/hap/camera/ch117_selected_stream.go b/pkg/hap/camera/ch117_selected_stream.go new file mode 100644 index 00000000..f6629c95 --- /dev/null +++ b/pkg/hap/camera/ch117_selected_stream.go @@ -0,0 +1,52 @@ +package camera + +const TypeSelectedStreamConfiguration = "117" + +type SelectedStreamConfig struct { + Control SessionControl `tlv8:"1"` + VideoParams SelectedVideoParams `tlv8:"2"` + AudioParams SelectedAudioParams `tlv8:"3"` +} + +const ( + SessionCommandEnd = 0 + SessionCommandStart = 1 + SessionCommandSuspend = 2 + SessionCommandResume = 3 + SessionCommandReconfigure = 4 +) + +type SessionControl struct { + Session string `tlv8:"1"` + Command byte `tlv8:"2"` +} + +type SelectedVideoParams struct { + CodecType byte `tlv8:"1"` // only 0 - H264 + CodecParams VideoCodecParams `tlv8:"2"` + VideoAttrs VideoAttrs `tlv8:"3"` + RTPParams VideoRTPParams `tlv8:"4"` +} + +type VideoRTPParams struct { + PayloadType uint8 `tlv8:"1"` + SSRC uint32 `tlv8:"2"` + MaxBitrate uint16 `tlv8:"3"` + MinRTCPInterval float32 `tlv8:"4"` + MaxMTU uint16 `tlv8:"5"` +} + +type SelectedAudioParams struct { + CodecType byte `tlv8:"1"` // 2 - AAC_ELD, 3 - OPUS, 5 - AMR, 6 - AMR_WB + CodecParams AudioCodecParams `tlv8:"2"` + RTPParams AudioRTPParams `tlv8:"3"` + ComfortNoise uint8 `tlv8:"4"` +} + +type AudioRTPParams struct { + PayloadType uint8 `tlv8:"1"` + SSRC uint32 `tlv8:"2"` + MaxBitrate uint16 `tlv8:"3"` + MinRTCPInterval float32 `tlv8:"4"` + ComfortNoisePayloadType uint8 `tlv8:"6"` +} diff --git a/pkg/hap/camera/ch118_setup_endpoints.go b/pkg/hap/camera/ch118_setup_endpoints.go new file mode 100644 index 00000000..efd5557f --- /dev/null +++ b/pkg/hap/camera/ch118_setup_endpoints.go @@ -0,0 +1,33 @@ +package camera + +const TypeSetupEndpoints = "118" + +type SetupEndpoints struct { + SessionID []byte `tlv8:"1"` + ControllerAddr Addr `tlv8:"3"` + VideoCrypto CryptoSuite `tlv8:"4"` + AudioCrypto CryptoSuite `tlv8:"5"` +} + +type Addr struct { + IPVersion byte `tlv8:"1"` + IPAddr string `tlv8:"2"` + VideoRTPPort uint16 `tlv8:"3"` + AudioRTPPort uint16 `tlv8:"4"` +} + +type CryptoSuite struct { + CryptoType byte `tlv8:"1"` + MasterKey []byte `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) + MasterSalt []byte `tlv8:"3"` // 14 byte +} + +type SetupEndpointsResponse struct { + SessionID []byte `tlv8:"1"` + Status byte `tlv8:"2"` + AccessoryAddr Addr `tlv8:"3"` + VideoCrypto CryptoSuite `tlv8:"4"` + AudioCrypto CryptoSuite `tlv8:"5"` + VideoSSRC uint32 `tlv8:"6"` + AudioSSRC uint32 `tlv8:"7"` +} diff --git a/pkg/hap/camera/ch120_streaming_status.go b/pkg/hap/camera/ch120_streaming_status.go new file mode 100644 index 00000000..6cde34e6 --- /dev/null +++ b/pkg/hap/camera/ch120_streaming_status.go @@ -0,0 +1,13 @@ +package camera + +const TypeStreamingStatus = "120" + +type StreamingStatus struct { + Status byte `tlv8:"1"` +} + +const ( + StreamingStatusAvailable = 0 + StreamingStatusBusy = 1 + StreamingStatusUnavailable = 2 +) diff --git a/pkg/hap/camera/client.go b/pkg/hap/camera/client.go index 1bcc413d..5e25b923 100644 --- a/pkg/hap/camera/client.go +++ b/pkg/hap/camera/client.go @@ -2,23 +2,22 @@ package camera import ( "errors" + "github.com/AlexxIT/go2rtc/pkg/hap" - "github.com/brutella/hap/characteristic" - "github.com/brutella/hap/rtp" ) type Client struct { - client *hap.Conn + client *hap.Client } -func NewClient(client *hap.Conn) *Client { +func NewClient(client *hap.Client) *Client { return &Client{client: client} } -func (c *Client) StartStream(ses *Session) (err error) { +func (c *Client) StartStream(ses *Session) error { // Step 1. Check if camera ready (free) to stream - var srv *hap.Service - if srv, err = c.GetFreeStream(); err != nil { + srv, err := c.GetFreeStream() + if err != nil { return err } if srv == nil { @@ -26,7 +25,7 @@ func (c *Client) StartStream(ses *Session) (err error) { } if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil { - return + return err } return c.SetConfig(srv, ses.Config) @@ -36,20 +35,20 @@ func (c *Client) StartStream(ses *Session) (err error) { // Usual every HomeKit camera can stream only to two clients simultaniosly. // So it has two similar services for streaming. func (c *Client) GetFreeStream() (srv *hap.Service, err error) { - var accs []*hap.Accessory - if accs, err = c.client.GetAccessories(); err != nil { + accs, err := c.client.GetAccessories() + if err != nil { return } for _, srv = range accs[0].Services { for _, char := range srv.Characters { - if char.Type == characteristic.TypeStreamingStatus { - status := rtp.StreamingStatus{} + if char.Type == TypeStreamingStatus { + var status StreamingStatus if err = char.ReadTLV8(&status); err != nil { return } - if status.Status == rtp.SessionStatusSuccess { + if status.Status == StreamingStatusAvailable { return } } @@ -59,11 +58,9 @@ func (c *Client) GetFreeStream() (srv *hap.Service, err error) { return nil, nil } -func (c *Client) SetupEndpoins( - srv *hap.Service, req *rtp.SetupEndpoints, -) (res *rtp.SetupEndpointsResponse, err error) { +func (c *Client) SetupEndpoins(srv *hap.Service, req *SetupEndpoints) (res *SetupEndpointsResponse, err error) { // get setup endpoint character ID - char := srv.GetCharacter(characteristic.TypeSetupEndpoints) + char := srv.GetCharacter(TypeSetupEndpoints) char.Event = nil // encode new character value if err = char.Write(req); err != nil { @@ -79,7 +76,7 @@ func (c *Client) SetupEndpoins( return } // decode new endpoint value - res = &rtp.SetupEndpointsResponse{} + res = &SetupEndpointsResponse{} if err = char.ReadTLV8(res); err != nil { return } @@ -87,13 +84,13 @@ func (c *Client) SetupEndpoins( return } -func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) { +func (c *Client) SetConfig(srv *hap.Service, config *SelectedStreamConfig) error { // get setup endpoint character ID - char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration) + char := srv.GetCharacter(TypeSelectedStreamConfiguration) char.Event = nil // encode new character value - if err = char.Write(config); err != nil { - panic(err) + if err := char.Write(config); err != nil { + return err } // write (put) new character value to device return c.client.PutCharacters(char) diff --git a/pkg/hap/camera/session.go b/pkg/hap/camera/session.go index b046933b..5ba3d4c1 100644 --- a/pkg/hap/camera/session.go +++ b/pkg/hap/camera/session.go @@ -1,75 +1,73 @@ package camera import ( - cryptorand "crypto/rand" + "crypto/rand" "encoding/binary" - "github.com/brutella/hap/rtp" ) type Session struct { - Offer *rtp.SetupEndpoints - Answer *rtp.SetupEndpointsResponse - Config *rtp.StreamConfiguration + Offer *SetupEndpoints + Answer *SetupEndpointsResponse + Config *SelectedStreamConfig } -func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session { - vp.RTP = rtp.RTPParams{ - PayloadType: 99, - Ssrc: RandomUint32(), - Bitrate: 2048, - Interval: 10, - MTU: 1200, // like WebRTC +func NewSession(vp *SelectedVideoParams, ap *SelectedAudioParams) *Session { + vp.RTPParams = VideoRTPParams{ + PayloadType: 99, + SSRC: RandomUint32(), + MaxBitrate: 2048, + MinRTCPInterval: 10, + MaxMTU: 1200, // like WebRTC } - ap.RTP = rtp.RTPParams{ + ap.RTPParams = AudioRTPParams{ PayloadType: 110, - Ssrc: RandomUint32(), - Bitrate: 32, - Interval: 10, + SSRC: RandomUint32(), + MaxBitrate: 32, + MinRTCPInterval: 10, ComfortNoisePayloadType: 98, - MTU: 0, } sessionID := RandomBytes(16) s := &Session{ - Offer: &rtp.SetupEndpoints{ - SessionId: sessionID, - Video: rtp.CryptoSuite{ + Offer: &SetupEndpoints{ + SessionID: sessionID, + VideoCrypto: CryptoSuite{ MasterKey: RandomBytes(16), MasterSalt: RandomBytes(14), }, - Audio: rtp.CryptoSuite{ + AudioCrypto: CryptoSuite{ MasterKey: RandomBytes(16), MasterSalt: RandomBytes(14), }, }, - Config: &rtp.StreamConfiguration{ - Command: rtp.SessionControlCommand{ - Identifier: sessionID, - Type: rtp.SessionControlCommandTypeStart, + Config: &SelectedStreamConfig{ + Control: SessionControl{ + Session: string(sessionID), + Command: SessionCommandStart, }, - Video: *vp, - Audio: *ap, + VideoParams: *vp, + AudioParams: *ap, }, } return s } func (s *Session) SetLocalEndpoint(host string, port uint16) { - s.Offer.ControllerAddr = rtp.Addr{ + s.Offer.ControllerAddr = Addr{ IPAddr: host, - VideoRtpPort: port, - AudioRtpPort: port, + VideoRTPPort: port, + AudioRTPPort: port, } } func RandomBytes(size int) []byte { data := make([]byte, size) - _, _ = cryptorand.Read(data) + _, _ = rand.Read(data) return data } func RandomUint32() uint32 { data := make([]byte, 4) - _, _ = cryptorand.Read(data) + _, _ = rand.Read(data) return binary.BigEndian.Uint32(data) } diff --git a/pkg/hap/chacha20poly1305/chacha20poly1305.go b/pkg/hap/chacha20poly1305/chacha20poly1305.go new file mode 100644 index 00000000..27a35d41 --- /dev/null +++ b/pkg/hap/chacha20poly1305/chacha20poly1305.go @@ -0,0 +1,51 @@ +package chacha20poly1305 + +import ( + "errors" + + "golang.org/x/crypto/chacha20poly1305" +) + +var ErrInvalidParams = errors.New("chacha20poly1305: invalid params") + +// Decrypt - decrypt without verify +func Decrypt(key32 []byte, nonce8 string, ciphertext []byte) ([]byte, error) { + return DecryptAndVerify(key32, nil, []byte(nonce8), ciphertext, nil) +} + +// Encrypt - encrypt without seal +func Encrypt(key32 []byte, nonce8 string, plaintext []byte) ([]byte, error) { + return EncryptAndSeal(key32, nil, []byte(nonce8), plaintext, nil) +} + +func DecryptAndVerify(key32, dst, nonce8, ciphertext, verify []byte) ([]byte, error) { + if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 { + return nil, ErrInvalidParams + } + + aead, err := chacha20poly1305.New(key32) + if err != nil { + return nil, err + } + + nonce := make([]byte, chacha20poly1305.NonceSize) + copy(nonce[4:], nonce8) + + return aead.Open(dst, nonce, ciphertext, verify) +} + +func EncryptAndSeal(key32, dst, nonce8, plaintext, verify []byte) ([]byte, error) { + if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 { + return nil, ErrInvalidParams + } + + aead, err := chacha20poly1305.New(key32) + if err != nil { + return nil, err + } + + nonce := make([]byte, chacha20poly1305.NonceSize) + copy(nonce[4:], nonce8) + + return aead.Seal(dst, nonce, plaintext, verify), nil +} diff --git a/pkg/hap/character.go b/pkg/hap/character.go index d7c68337..6fc1c64f 100644 --- a/pkg/hap/character.go +++ b/pkg/hap/character.go @@ -2,12 +2,11 @@ package hap import ( "bytes" - "encoding/base64" "encoding/json" - "github.com/brutella/hap/characteristic" - "github.com/brutella/hap/tlv8" "io" "net/http" + + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) type Character struct { @@ -50,7 +49,7 @@ func (c *Character) NotifyListeners(ignore io.Writer) error { return err } - for w, _ := range c.listeners { + for w := range c.listeners { if w == ignore { continue } @@ -101,19 +100,15 @@ func (c *Character) Set(v any) (err error) { // Write new value with right format func (c *Character) Write(v any) (err error) { switch c.Format { - case characteristic.FormatTLV8: - var data []byte - if data, err = tlv8.Marshal(v); err != nil { - return - } - c.Value = base64.StdEncoding.EncodeToString(data) + case "tlv8": + c.Value, err = tlv8.MarshalBase64(v) - case characteristic.FormatBool: - switch v.(type) { + case "bool": + switch v := v.(type) { case bool: - c.Value = v.(bool) + c.Value = v case float64: - c.Value = v.(float64) != 0 + c.Value = v != 0 } } return @@ -121,13 +116,17 @@ func (c *Character) Write(v any) (err error) { // ReadTLV8 value to right struct func (c *Character) ReadTLV8(v any) (err error) { - var data []byte - if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil { - return - } - return tlv8.Unmarshal(data, v) + return tlv8.UnmarshalBase64(c.Value.(string), v) } func (c *Character) ReadBool() bool { return c.Value.(bool) } + +func (c *Character) String() string { + data, err := json.Marshal(c) + if err != nil { + return "ERROR" + } + return string(data) +} diff --git a/pkg/hap/client.go b/pkg/hap/client.go new file mode 100644 index 00000000..f02ce7d8 --- /dev/null +++ b/pkg/hap/client.go @@ -0,0 +1,328 @@ +package hap + +import ( + "bufio" + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" + "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" + "github.com/AlexxIT/go2rtc/pkg/hap/secure" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/AlexxIT/go2rtc/pkg/mdns" +) + +const ( + ConnDialTimeout = time.Second * 3 + ConnDeadline = time.Second * 3 +) + +// Client for HomeKit. DevicePublic can be null. +type Client struct { + DeviceAddress string // including port + DeviceID string // aka. Accessory + DevicePublic []byte + ClientID string // aka. Controller + ClientPrivate []byte + + OnEvent func(res *http.Response) + Output func(msg any) + + conn net.Conn + reader *bufio.Reader +} + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + c := &Client{ + DeviceAddress: u.Host, + DeviceID: query.Get("device_id"), + DevicePublic: DecodeKey(query.Get("device_public")), + ClientID: query.Get("client_id"), + ClientPrivate: DecodeKey(query.Get("client_private")), + } + + return c, nil +} + +func (c *Client) ClientPublic() []byte { + return c.ClientPrivate[32:] +} + +func (c *Client) URL() string { + return fmt.Sprintf( + "homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x", + c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate, + ) +} + +func (c *Client) DeviceHost() string { + if i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 { + return c.DeviceAddress[:i] + } + return c.DeviceAddress +} + +func (c *Client) Dial() (err error) { + // update device address (host and/or port) before dial + _ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { + if entry.Complete() && entry.Info["id"] == c.DeviceID { + c.DeviceAddress = entry.Addr() + return true + } + return false + }) + + if c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { + return + } + + c.reader = bufio.NewReader(c.conn) + + // STEP M1: send our session public to device + sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() + + // 1. Send sessionPublic + plainM1 := struct { + PublicKey []byte `tlv8:"3"` + State byte `tlv8:"6"` + }{ + PublicKey: sessionPublic, + State: StateM1, + } + res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1)) + if err != nil { + return + } + + // STEP M2: unpack deviceID from response + var cipherM2 struct { + PublicKey []byte `tlv8:"3"` + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil { + return err + } + if cipherM2.State != StateM2 { + return NewResponseError(plainM1, cipherM2) + } + + // 1. generate session shared key + sessionShared, err := curve25519.SharedSecret(sessionPrivate, cipherM2.PublicKey) + if err != nil { + return + } + + sessionKey, err := hkdf.Sha512( + sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", + ) + if err != nil { + return + } + + // 2. decrypt M2 response with session key + b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", cipherM2.EncryptedData) + if err != nil { + return + } + + // 3. unpack payload from TLV8 + var plainM2 struct { + Identifier string `tlv8:"1"` + Signature []byte `tlv8:"10"` + } + if err = tlv8.Unmarshal(b, &plainM2); err != nil { + return + } + + // 4. verify signature for M2 response with device public + // device session + device id + our session + if c.DevicePublic != nil { + b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic) + if !ed25519.ValidateSignature(c.DevicePublic, b, plainM2.Signature) { + return errors.New("hap: ValidateSignature") + } + } + + // STEP M3: send our clientID to device + // 1. generate signature with our private key + // (our session + our ID + device session) + b = Append(sessionPublic, c.ClientID, cipherM2.PublicKey) + if b, err = ed25519.Signature(c.ClientPrivate, b); err != nil { + return + } + + // 2. generate payload + plainM3 := struct { + Identifier string `tlv8:"1"` + Signature []byte `tlv8:"10"` + }{ + Identifier: c.ClientID, + Signature: b, + } + if b, err = tlv8.Marshal(plainM3); err != nil { + return + } + + // 4. encrypt payload with session key + if b, err = chacha20poly1305.Encrypt(sessionKey, "PV-Msg03", b); err != nil { + return + } + + // 4. generate request + cipherM3 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + }{ + State: StateM3, + EncryptedData: b, + } + if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil { + return + } + + // STEP M4. Read response + var plainM4 struct { + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + return + } + if plainM4.State != StateM4 { + return NewResponseError(cipherM3, plainM4) + } + + // like tls.Client wrapper over net.Conn + if c.conn, err = secure.Client(c.conn, sessionShared, true); err != nil { + return + } + // new reader for new conn + c.reader = bufio.NewReaderSize(c.conn, 32*1024) // 32K like default request body + + return +} + +func (c *Client) Close() error { + if c.conn == nil { + return nil + } + conn := c.conn + c.conn = nil + return conn.Close() +} + +func (c *Client) GetAccessories() ([]*Accessory, error) { + res, err := c.Get(PathAccessories) + if err != nil { + return nil, err + } + + var ac Accessories + if err = json.NewDecoder(res.Body).Decode(&ac); err != nil { + return nil, err + } + + for _, accs := range ac.Accessories { + for _, serv := range accs.Services { + for _, char := range serv.Characters { + char.AID = accs.AID + } + } + } + + return ac.Accessories, nil +} + +func (c *Client) GetCharacters(query string) ([]*Character, error) { + res, err := c.Get(PathCharacteristics + "?id=" + query) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var ch Characters + if err = json.Unmarshal(data, &ch); err != nil { + return nil, err + } + return ch.Characters, nil +} + +func (c *Client) GetCharacter(char *Character) error { + query := fmt.Sprintf("%d.%d", char.AID, char.IID) + chars, err := c.GetCharacters(query) + if err != nil { + return err + } + char.Value = chars[0].Value + return nil +} + +func (c *Client) PutCharacters(characters ...*Character) error { + for i, char := range characters { + if char.Event != nil { + char = &Character{AID: char.AID, IID: char.IID, Event: char.Event} + } else { + char = &Character{AID: char.AID, IID: char.IID, Value: char.Value} + } + characters[i] = char + } + data, err := json.Marshal(Characters{characters}) + if err != nil { + return err + } + + _, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(data)) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetImage(width, height int) ([]byte, error) { + s := fmt.Sprintf( + `{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`, + width, height, + ) + res, err := c.Post(PathResource, MimeJSON, bytes.NewBufferString(s)) + if err != nil { + return nil, err + } + return io.ReadAll(res.Body) +} + +func (c *Client) LocalAddr() string { + return c.conn.LocalAddr().String() +} + +func DecodeKey(s string) []byte { + if s == "" { + return nil + } + data, err := hex.DecodeString(s) + if err != nil { + return nil + } + return data +} diff --git a/pkg/hap/client_http.go b/pkg/hap/client_http.go new file mode 100644 index 00000000..c3464fb4 --- /dev/null +++ b/pkg/hap/client_http.go @@ -0,0 +1,60 @@ +package hap + +import ( + "errors" + "io" + "net/http" + "time" +) + +const ( + MimeTLV8 = "application/pairing+tlv8" + MimeJSON = "application/hap+json" + + PathPairSetup = "/pair-setup" + PathPairVerify = "/pair-verify" + PathPairings = "/pairings" + PathAccessories = "/accessories" + PathCharacteristics = "/characteristics" + PathResource = "/resource" +) + +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if err := c.conn.SetWriteDeadline(time.Now().Add(ConnDeadline)); err != nil { + return nil, err + } + if err := req.Write(c.conn); err != nil { + return nil, err + } + return http.ReadResponse(c.reader, req) +} + +func (c *Client) Request(method, path, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, "http://"+c.DeviceAddress+path, body) + if err != nil { + return nil, err + } + + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + res, err := c.Do(req) + if err == nil && res.StatusCode >= http.StatusBadRequest { + err = errors.New("hap: wrong http status: " + res.Status) + } + + return res, err +} + +func (c *Client) Get(path string) (*http.Response, error) { + return c.Request("GET", path, "", nil) +} + +func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response, error) { + return c.Request("POST", path, contentType, body) +} + +func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) { + return c.Request("PUT", path, contentType, body) +} diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go new file mode 100644 index 00000000..56a2cd1a --- /dev/null +++ b/pkg/hap/client_pairing.go @@ -0,0 +1,376 @@ +package hap + +import ( + "bufio" + "crypto/sha512" + "errors" + "fmt" + "net" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/AlexxIT/go2rtc/pkg/mdns" + "github.com/tadglines/go-pkgs/crypto/srp" +) + +func Pair(deviceID, pin string) (*Client, error) { + var addr string + var mfi bool + + _ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { + if entry.Complete() && entry.Info["id"] == deviceID { + addr = entry.Addr() + mfi = entry.Info["ff"] == "1" + return true + } + return false + }) + + if addr == "" { + return nil, errors.New("hap: mdns.Discovery") + } + + c := &Client{ + DeviceAddress: addr, + DeviceID: deviceID, + ClientID: GenerateUUID(), + ClientPrivate: GenerateKey(), + } + + return c, c.Pair(mfi, pin) +} + +func (c *Client) Pair(mfi bool, pin string) (err error) { + pin = strings.ReplaceAll(pin, "-", "") + if len(pin) != 8 { + return fmt.Errorf("wrong PIN format: %s", pin) + } + + pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] // 123-45-678 + + c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout) + if err != nil { + return + } + + c.reader = bufio.NewReader(c.conn) + + // STEP M1. Send HELLO + plainM1 := struct { + Method byte `tlv8:"0"` + State byte `tlv8:"6"` + }{ + Method: MethodPair, + State: StateM1, + } + if mfi { + plainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0 + } + res, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1)) + if err != nil { + return + } + + // STEP M2. Read Device Salt and session PublicKey + var plainM2 struct { + Salt []byte `tlv8:"2"` + SessionKey []byte `tlv8:"3"` // server public key, aka session.B + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + return + } + if plainM2.State != StateM2 { + return NewResponseError(plainM1, plainM2) + } + if plainM2.Error != 0 { + return newPairingError(plainM2.Error) + } + + // STEP M3. Generate SRP Session using pin + username := []byte("Pair-Setup") + + // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) + pake, err := srp.NewSRP( + "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), + ) + if err != nil { + return + } + + pake.SaltLength = 16 + + // username: "Pair-Setup", password: PIN (with dashes) + session := pake.NewClientSession(username, []byte(pin)) + sessionShared, err := session.ComputeKey(plainM2.Salt, plainM2.SessionKey) + if err != nil { + return + } + + // STEP M3. Send request + plainM3 := struct { + SessionKey []byte `tlv8:"3"` + Proof []byte `tlv8:"4"` + State byte `tlv8:"6"` + }{ + SessionKey: session.GetA(), // client public key, aka session.A + Proof: session.ComputeAuthenticator(), + State: StateM3, + } + if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil { + return + } + + // STEP M4. Read response + var plainM4 struct { + Proof []byte `tlv8:"4"` // server proof + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + return + } + if plainM4.State != StateM4 { + return NewResponseError(plainM3, plainM4) + } + if plainM4.Error != 0 { + return newPairingError(plainM4.Error) + } + + // STEP M4. Verify response + if !session.VerifyServerAuthenticator(plainM4.Proof) { + return errors.New("hap: wrong server auth") + } + + // STEP M5. Generate signature + localSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", + ) + if err != nil { + return + } + + b := Append(localSign, c.ClientID, c.ClientPublic()) + signature, err := ed25519.Signature(c.ClientPrivate, b) + if err != nil { + return + } + + // STEP M5. Generate payload + plainM5 := struct { + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{ + Identifier: c.ClientID, + PublicKey: c.ClientPublic(), + Signature: signature, + } + if b, err = tlv8.Marshal(plainM5); err != nil { + return + } + + // STEP M5. Encrypt payload + encryptKey, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", + ) + if err != nil { + return + } + + if b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg05", b); err != nil { + return + } + + // STEP M5. Send request + cipherM5 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + }{ + EncryptedData: b, + State: StateM5, + } + if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil { + return + } + + // STEP M6. Read response + cipherM6 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + }{} + if err = tlv8.UnmarshalReader(res.Body, &cipherM6); err != nil { + return + } + if cipherM6.State != StateM6 || cipherM6.Error != 0 { + return NewResponseError(plainM5, cipherM6) + } + + // STEP M6. Decrypt payload + b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", cipherM6.EncryptedData) + if err != nil { + return + } + + plainM6 := struct { + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{} + if err = tlv8.Unmarshal(b, &plainM6); err != nil { + return + } + + // STEP M6. Verify payload + remoteSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", + ) + if err != nil { + return + } + + b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey) + if !ed25519.ValidateSignature(plainM6.PublicKey, b, plainM6.Signature) { + return errors.New("hap: wrong accessory sign") + } + + if c.DeviceID != plainM6.Identifier { + return errors.New("hap: wrong DeviceID: " + plainM6.Identifier) + } + + c.DevicePublic = plainM6.PublicKey + + return nil +} + +func (c *Client) ListPairings() error { + plainM1 := struct { + Method byte `tlv8:"0"` + State byte `tlv8:"6"` + }{ + Method: MethodListPairings, + State: StateM1, + } + res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1)) + if err != nil { + return err + } + + // TODO: don't know how to fix array of items + var plainM2 struct { + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + State byte `tlv8:"6"` + Permission byte `tlv8:"11"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + return err + } + + return nil +} + +func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error { + plainM1 := struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + State byte `tlv8:"6"` + Permission byte `tlv8:"11"` + }{ + Method: MethodAddPairing, + Identifier: clientID, + PublicKey: clientPublic, + State: StateM1, + Permission: PermissionUser, + } + if admin { + plainM1.Permission = PermissionAdmin + } + res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1)) + if err != nil { + return err + } + + var plainM2 struct { + State byte `tlv8:"6"` + Unknown byte `tlv8:"7"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + return err + } + + return nil +} + +func (c *Client) DeletePairing(id string) error { + plainM1 := struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + State byte `tlv8:"6"` + }{ + Method: MethodDeletePairing, + Identifier: id, + State: StateM1, + } + res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1)) + if err != nil { + return err + } + + var plainM2 struct { + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + return err + } + if plainM2.State != StateM2 { + return NewResponseError(plainM1, plainM2) + } + + return nil +} + +func newPairingError(code byte) error { + var text string + // https://github.com/apple/HomeKitADK/blob/fb201f98f5fdc7fef6a455054f08b59cca5d1ec8/HAP/HAPPairing.h#L89 + switch code { + case 1: + text = "Generic error to handle unexpected errors" + case 2: + text = "Setup code or signature verification failed" + case 3: + text = "Client must look at the retry delay TLV item and wait that many seconds before retrying" + case 4: + text = "Server cannot accept any more pairings" + case 5: + text = "Server reached its maximum number of authentication attempts" + case 6: + text = "Server pairing method is unavailable" + case 7: + text = "Server is busy and cannot accept a pairing request at this time" + default: + text = "Unknown pairing error" + } + return errors.New("hap: " + text) +} + +func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc { + return func(salt, password []byte) []byte { + h1 := sha512.New() + h1.Write(username) + h1.Write([]byte(":")) + h1.Write(password) + + h2 := sha512.New() + h2.Write(salt) + h2.Write(h1.Sum(nil)) + + return h2.Sum(nil) + } +} diff --git a/pkg/hap/conn.go b/pkg/hap/conn.go deleted file mode 100644 index 34c140b9..00000000 --- a/pkg/hap/conn.go +++ /dev/null @@ -1,746 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/sha512" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "strings" - "time" - - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/mdns" - "github.com/brutella/hap" - "github.com/brutella/hap/chacha20poly1305" - "github.com/brutella/hap/curve25519" - "github.com/brutella/hap/ed25519" - "github.com/brutella/hap/hkdf" - "github.com/brutella/hap/tlv8" - "github.com/tadglines/go-pkgs/crypto/srp" -) - -// Conn for HomeKit. DevicePublic can be null. -type Conn struct { - core.Listener - - DeviceAddress string // including port - DeviceID string - DevicePublic []byte - ClientID string - ClientPrivate []byte - - OnEvent func(res *http.Response) - Output func(msg any) - - conn net.Conn - secure *Secure - httpResponse chan *bufio.Reader -} - -func NewConn(rawURL string) (*Conn, error) { - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } - - query := u.Query() - c := &Conn{ - DeviceAddress: u.Host, - DeviceID: query.Get("device_id"), - DevicePublic: DecodeKey(query.Get("device_public")), - ClientID: query.Get("client_id"), - ClientPrivate: DecodeKey(query.Get("client_private")), - } - - return c, nil -} - -func Pair(deviceID, pin string) (*Conn, error) { - var addr string - var mfi bool - - _ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { - if entry.Complete() && entry.Info["id"] == deviceID { - addr = entry.Addr() - mfi = entry.Info["ff"] == "1" - return true - } - return false - }) - - if addr == "" { - return nil, errors.New("can't find device via mDNS") - } - - c := &Conn{ - DeviceAddress: addr, - DeviceID: deviceID, - ClientID: GenerateUUID(), - ClientPrivate: GenerateKey(), - } - - return c, c.Pair(mfi, pin) -} - -func (c *Conn) ClientPublic() []byte { - return c.ClientPrivate[32:] -} - -func (c *Conn) URL() string { - return fmt.Sprintf( - "homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x", - c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate, - ) -} - -func (c *Conn) DialAndServe() error { - if err := c.Dial(); err != nil { - return err - } - return c.Handle() -} - -func (c *Conn) DeviceHost() string { - if i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 { - return c.DeviceAddress[:i] - } - return c.DeviceAddress -} - -func (c *Conn) Dial() error { - // update device address (host and/or port) before dial - _ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { - if entry.Complete() && entry.Info["id"] == c.DeviceID { - c.DeviceAddress = entry.Addr() - return true - } - return false - }) - - var err error - c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5) - if err != nil { - return err - } - - // STEP M1: send our session public to device - sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() - - // 1. generate payload - // important not include other fields - requestM1 := struct { - State byte `tlv8:"6"` - PublicKey []byte `tlv8:"3"` - }{ - State: hap.M1, - PublicKey: sessionPublic[:], - } - // 2. pack payload to TLV8 - buf, err := tlv8.Marshal(requestM1) - if err != nil { - return err - } - - // 3. send request - resp, err := c.Post(UriPairVerify, buf) - if err != nil { - return err - } - - // STEP M2: unpack deviceID from response - responseM2 := PairVerifyPayload{} - if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil { - return err - } - - // 1. generate session shared key - var deviceSessionPublic [32]byte - copy(deviceSessionPublic[:], responseM2.PublicKey) - sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic) - sessionKey, err := hkdf.Sha512( - sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"), - []byte("Pair-Verify-Encrypt-Info"), - ) - - // 2. decrypt M2 response with session key - msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16] - var mac [16]byte - copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC) - - buf, err = chacha20poly1305.DecryptAndVerify( - sessionKey[:], []byte("PV-Msg02"), msg, mac, nil, - ) - - // 3. unpack payload from TLV8 - payloadM2 := PairVerifyPayload{} - if err = tlv8.Unmarshal(buf, &payloadM2); err != nil { - return err - } - - // 4. verify signature for M2 response with device public - // device session + device id + our session - if c.DevicePublic != nil { - buf = nil - buf = append(buf, responseM2.PublicKey[:]...) - buf = append(buf, []byte(payloadM2.Identifier)...) - buf = append(buf, sessionPublic[:]...) - if !ed25519.ValidateSignature( - c.DevicePublic[:], buf, payloadM2.Signature, - ) { - return errors.New("device public signature invalid") - } - } - - // STEP M3: send our clientID to device - // 1. generate signature with our private key - // (our session + our ID + device session) - buf = nil - buf = append(buf, sessionPublic[:]...) - buf = append(buf, []byte(c.ClientID)...) - buf = append(buf, responseM2.PublicKey[:]...) - signature, err := ed25519.Signature(c.ClientPrivate[:], buf) - if err != nil { - return err - } - - // 2. generate payload - payloadM3 := struct { - Identifier string `tlv8:"1"` - Signature []byte `tlv8:"10"` - }{ - Identifier: c.ClientID, - Signature: signature, - } - // 3. pack payload to TLV8 - buf, err = tlv8.Marshal(payloadM3) - if err != nil { - return err - } - - // 4. encrypt payload with session key - msg, mac, _ = chacha20poly1305.EncryptAndSeal( - sessionKey[:], []byte("PV-Msg03"), buf, nil, - ) - - // 4. generate request - requestM3 := struct { - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - }{ - State: hap.M3, - EncryptedData: append(msg, mac[:]...), - } - // 5. pack payload to TLV8 - buf, err = tlv8.Marshal(requestM3) - if err != nil { - return err - } - - resp, err = c.Post(UriPairVerify, buf) - if err != nil { - return err - } - - // STEP M4. Read response - responseM4 := PairVerifyPayload{} - if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil { - return err - } - - // 1. check response state - if responseM4.State != 4 || responseM4.Status != 0 { - return fmt.Errorf("wrong M4 response: %+v", responseM4) - } - - c.secure, err = NewSecure(sessionShared, false) - //c.secure.Buffer = bytes.NewBuffer(nil) - c.secure.Conn = c.conn - - c.httpResponse = make(chan *bufio.Reader, 10) - - return err -} - -// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c -func (c *Conn) Pair(mfi bool, pin string) (err error) { - pin = strings.ReplaceAll(pin, "-", "") - if len(pin) != 8 { - return fmt.Errorf("wrong PIN format: %s", pin) - } - pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] - - c.conn, err = net.Dial("tcp", c.DeviceAddress) - if err != nil { - return - } - - // STEP M1. Generate request - reqM1 := struct { - Method byte `tlv8:"0"` - State byte `tlv8:"6"` - }{ - State: hap.M1, - } - if mfi { - reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0 - } - buf, err := tlv8.Marshal(reqM1) - if err != nil { - return - } - - // STEP M1. Send request - res, err := c.Post(UriPairSetup, buf) - if err != nil { - return - } - - // STEP M2. Read response - resM2 := struct { - Salt []byte `tlv8:"2"` - PublicKey []byte `tlv8:"3"` // server public key, aka session.B - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{} - if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil { - return - } - if resM2.State != 2 || resM2.Error > 0 { - return fmt.Errorf("wrong M2: %+v", resM2) - } - - // STEP M3. Generate session using pin - username := []byte("Pair-Setup") - - SRP, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) - if err != nil { - return - } - - SRP.SaltLength = 16 - - // username: "Pair-Setup" - // password: PIN (with dashes) - session := SRP.NewClientSession(username, []byte(pin)) - sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey) - if err != nil { - return - } - - // STEP M3. Generate request - reqM3 := struct { - PublicKey []byte `tlv8:"3"` - Proof []byte `tlv8:"4"` - State byte `tlv8:"6"` - }{ - PublicKey: session.GetA(), // client public key, aka session.A - Proof: session.ComputeAuthenticator(), - State: hap.M3, - } - buf, err = tlv8.Marshal(reqM3) - if err != nil { - return err - } - - // STEP M3. Send request - res, err = c.Post(UriPairSetup, buf) - if err != nil { - return - } - - // STEP M4. Read response - resM4 := struct { - Proof []byte `tlv8:"4"` // server proof - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{} - if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil { - return - } - if resM4.Error == 2 { - return fmt.Errorf("wrong PIN: %s", pin) - } - if resM4.State != 4 || resM4.Error > 0 { - return fmt.Errorf("wrong M4: %+v", resM4) - } - - // STEP M4. Verify response - if !session.VerifyServerAuthenticator(resM4.Proof) { - return errors.New("verify server auth fail") - } - - // STEP M5. Generate signature - saltKey, err := hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"), - []byte("Pair-Setup-Controller-Sign-Info"), - ) - if err != nil { - return - } - - buf = nil - buf = append(buf, saltKey[:]...) - buf = append(buf, []byte(c.ClientID)...) - buf = append(buf, c.ClientPublic()...) - - signature, err := ed25519.Signature(c.ClientPrivate, buf) - if err != nil { - return - } - - // STEP M5. Generate payload - msgM5 := struct { - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` - }{ - Identifier: c.ClientID, - PublicKey: c.ClientPublic(), - Signature: signature, - } - buf, err = tlv8.Marshal(msgM5) - if err != nil { - return - } - - // STEP M5. Encrypt payload - sessionKey, err := hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Encrypt-Salt"), - []byte("Pair-Setup-Encrypt-Info"), - ) - if err != nil { - return - } - buf, mac, _ := chacha20poly1305.EncryptAndSeal( - sessionKey[:], []byte("PS-Msg05"), buf, nil, - ) - - // STEP M5. Generate request - reqM5 := struct { - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - }{ - EncryptedData: append(buf, mac[:]...), - State: hap.M5, - } - buf, err = tlv8.Marshal(reqM5) - if err != nil { - return err - } - - // STEP M5. Send request - res, err = c.Post(UriPairSetup, buf) - if err != nil { - return - } - - // STEP M6. Read response - resM6 := struct { - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{} - if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil { - return - } - if resM6.State != 6 || resM6.Error > 0 { - return fmt.Errorf("wrong M6: %+v", resM2) - } - - // STEP M6. Decrypt payload - msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16] - copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC) - - buf, err = chacha20poly1305.DecryptAndVerify( - sessionKey[:], []byte("PS-Msg06"), msg, mac, nil, - ) - if err != nil { - return - } - - msgM6 := struct { - Identifier []byte `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` - }{} - if err = tlv8.Unmarshal(buf, &msgM6); err != nil { - return - } - - // STEP M6. Verify payload - if saltKey, err = hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"), - []byte("Pair-Setup-Accessory-Sign-Info"), - ); err != nil { - return - } - - buf = nil - buf = append(buf, saltKey[:]...) - buf = append(buf, msgM6.Identifier...) - buf = append(buf, msgM6.PublicKey...) - - if !ed25519.ValidateSignature( - msgM6.PublicKey[:], buf, msgM6.Signature, - ) { - return errors.New("wrong server signature") - } - - if c.DeviceID != string(msgM6.Identifier) { - return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier) - } - - c.DevicePublic = msgM6.PublicKey - - return nil -} - -func (c *Conn) Close() error { - if c.conn == nil { - return nil - } - conn := c.conn - c.conn = nil - return conn.Close() -} - -func (c *Conn) GetAccessories() ([]*Accessory, error) { - res, err := c.Get("/accessories") - if err != nil { - return nil, err - } - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - p := Accessories{} - if err = json.Unmarshal(data, &p); err != nil { - return nil, err - } - - for _, accs := range p.Accessories { - for _, serv := range accs.Services { - for _, char := range serv.Characters { - char.AID = accs.AID - } - } - } - - return p.Accessories, nil -} - -func (c *Conn) GetCharacters(query string) ([]*Character, error) { - res, err := c.Get("/characteristics?id=" + query) - if err != nil { - return nil, err - } - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - ch := Characters{} - if err = json.Unmarshal(data, &ch); err != nil { - return nil, err - } - return ch.Characters, nil -} - -func (c *Conn) GetCharacter(char *Character) error { - query := fmt.Sprintf("%d.%d", char.AID, char.IID) - chars, err := c.GetCharacters(query) - if err != nil { - return err - } - char.Value = chars[0].Value - return nil -} - -func (c *Conn) PutCharacters(characters ...*Character) (err error) { - for i, char := range characters { - if char.Event != nil { - char = &Character{AID: char.AID, IID: char.IID, Event: char.Event} - } else { - char = &Character{AID: char.AID, IID: char.IID, Value: char.Value} - } - characters[i] = char - } - var data []byte - if data, err = json.Marshal(Characters{characters}); err != nil { - return - } - - var res *http.Response - if res, err = c.Put("/characteristics", data); err != nil { - return - } - - if res.StatusCode >= 400 { - return errors.New("wrong response status") - } - - return -} - -func (c *Conn) GetImage(width, height int) ([]byte, error) { - res, err := c.Post( - "/resource", []byte(fmt.Sprintf( - `{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`, - width, height, - )), - ) - if err != nil { - return nil, err - } - return io.ReadAll(res.Body) -} - -//func (c *Client) onEventData(r io.Reader) error { -// if c.OnEvent == nil { -// return nil -// } -// -// data, err := io.ReadAll(r) -// -// ch := Characters{} -// if err = json.Unmarshal(data, &ch); err != nil { -// return err -// } -// -// c.OnEvent(ch.Characters) -// -// return nil -//} - -func (c *Conn) ListPairings() error { - pReq := struct { - Method byte `tlv8:"0"` - State byte `tlv8:"6"` - }{ - Method: hap.MethodListPairings, - State: hap.M1, - } - data, err := tlv8.Marshal(pReq) - if err != nil { - return err - } - - res, err := c.Post("/pairings", data) - if err != nil { - return err - } - - data, err = io.ReadAll(res.Body) - // TODO: don't know how to fix array of items - var pRes struct { - State byte `tlv8:"6"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Permission byte `tlv8:"11"` - } - if err = tlv8.Unmarshal(data, &pRes); err != nil { - return err - } - - return nil -} - -func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error { - pReq := struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - State byte `tlv8:"6"` - Permission byte `tlv8:"11"` - }{ - Method: hap.MethodAddPairing, - Identifier: clientID, - PublicKey: clientPublic, - State: hap.M1, - Permission: hap.PermissionUser, - } - if admin { - pReq.Permission = hap.PermissionAdmin - } - - data, err := tlv8.Marshal(pReq) - if err != nil { - return err - } - - res, err := c.Post("/pairings", data) - if err != nil { - return err - } - - data, err = io.ReadAll(res.Body) - var pRes struct { - State byte `tlv8:"6"` - Unknown byte `tlv8:"7"` - } - if err = tlv8.Unmarshal(data, &pRes); err != nil { - return err - } - - return nil -} - -func (c *Conn) DeletePairing(id string) error { - reqM1 := struct { - State byte `tlv8:"6"` - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - }{ - State: hap.M1, - Method: hap.MethodDeletePairing, - Identifier: id, - } - data, err := tlv8.Marshal(reqM1) - if err != nil { - return err - } - - res, err := c.Post("/pairings", data) - if err != nil { - return err - } - - data, err = io.ReadAll(res.Body) - var resM2 struct { - State byte `tlv8:"6"` - } - if err = tlv8.Unmarshal(data, &resM2); err != nil { - return err - } - if resM2.State != hap.M2 { - return errors.New("wrong state") - } - - return nil -} - -func (c *Conn) LocalAddr() string { - return c.conn.LocalAddr().String() -} - -func DecodeKey(s string) []byte { - if s == "" { - return nil - } - data, err := hex.DecodeString(s) - if err != nil { - return nil - } - return data -} diff --git a/pkg/hap/curve25519/curve25519.go b/pkg/hap/curve25519/curve25519.go new file mode 100644 index 00000000..f73f76d8 --- /dev/null +++ b/pkg/hap/curve25519/curve25519.go @@ -0,0 +1,18 @@ +package curve25519 + +import ( + "crypto/rand" + + "golang.org/x/crypto/curve25519" +) + +func GenerateKeyPair() ([]byte, []byte) { + var publicKey, privateKey [32]byte + _, _ = rand.Read(privateKey[:]) + curve25519.ScalarBaseMult(&publicKey, &privateKey) + return publicKey[:], privateKey[:] +} + +func SharedSecret(privateKey, otherPublicKey []byte) ([]byte, error) { + return curve25519.X25519(privateKey, otherPublicKey) +} diff --git a/pkg/hap/ed25519/ed25519.go b/pkg/hap/ed25519/ed25519.go new file mode 100644 index 00000000..646ce26f --- /dev/null +++ b/pkg/hap/ed25519/ed25519.go @@ -0,0 +1,24 @@ +package ed25519 + +import ( + "crypto/ed25519" + "errors" +) + +var ErrInvalidParams = errors.New("ed25519: invalid params") + +func ValidateSignature(key, data, signature []byte) bool { + if len(key) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize { + return false + } + + return ed25519.Verify(key, data, signature) +} + +func Signature(key, data []byte) ([]byte, error) { + if len(key) != ed25519.PrivateKeySize { + return nil, ErrInvalidParams + } + + return ed25519.Sign(key, data), nil +} diff --git a/pkg/hap/event_reader.go b/pkg/hap/event_reader.go new file mode 100644 index 00000000..d2303b6c --- /dev/null +++ b/pkg/hap/event_reader.go @@ -0,0 +1,68 @@ +package hap + +import ( + "io" + "os" + "time" +) + +type EventReader struct { + r io.Reader + ch chan []byte + err error + left []byte +} + +func NewEventReader(r io.Reader) *EventReader { + e := &EventReader{r: r, ch: make(chan []byte, 1)} + go e.background() + return e +} + +func (e *EventReader) background() { + b := make([]byte, 32*1024) + for { + n, err := e.r.Read(b) + if err != nil { + e.err = err + return + } + + if n >= 6 && string(b[:6]) == "EVENT " { + panic("TODO") + } + + // copy because will be overwriten + buf := make([]byte, n) + copy(buf, b) + e.ch <- buf + } +} + +func (e *EventReader) Read(p []byte) (n int, err error) { + if e.err != nil { + return 0, e.err + } + + // if something left after previous reading + if e.left != nil { + // if still something left + if n = copy(p, e.left); n < len(e.left) { + e.left = e.left[n:] + } else { + e.left = nil + } + return + } + + select { + case <-time.After(time.Second * 5): + return 0, os.ErrDeadlineExceeded + case b := <-e.ch: + if n = copy(p, b); n < len(b) { + e.left = b[n:] + } + } + + return +} diff --git a/pkg/hap/helpers.go b/pkg/hap/helpers.go index 46e4c7ad..b23a4975 100644 --- a/pkg/hap/helpers.go +++ b/pkg/hap/helpers.go @@ -1,6 +1,7 @@ package hap import ( + "crypto/ed25519" "crypto/rand" "crypto/sha512" "encoding/hex" @@ -10,8 +11,52 @@ import ( "net/http" ) +const ( + TXTConfigNumber = "c#" // Current configuration number (ex. 1, 2, 3) + TXTDeviceID = "id" // Device ID of the accessory (ex. 77:75:87:A0:7D:F4) + TXTModel = "md" // Model name of the accessory (ex. MJCTD02YL) + TXTProtoVersion = "pv" // Protocol version string (ex. 1.1) + TXTStateNumber = "s#" // Current state number (ex. 1) + TXTCategory = "ci" // Accessory Category Identifier (ex. 2, 5, 17) + TXTSetupHash = "sh" // Setup hash (ex. Y9w9hQ==) + + // TXTFeatureFlags + // - 0001b - Supports Apple Authentication Coprocessor + // - 0010b - Supports Software Authentication + TXTFeatureFlags = "ff" // Pairing Feature flags (ex. 0, 1, 2) + + // TXTStatusFlags + // - 0001b - Accessory has not been paired with any controllers + // - 0100b - A problem has been detected on the accessory + TXTStatusFlags = "sf" // Status flags (ex. 0, 1) + + StateM1 = 1 + StateM2 = 2 + StateM3 = 3 + StateM4 = 4 + StateM5 = 5 + StateM6 = 6 + + MethodPair = 0 + MethodPairMFi = 1 // if device has MFI cert + MethodVerifyPair = 2 + MethodAddPairing = 3 + MethodDeletePairing = 4 + MethodListPairings = 5 +) + +const ( + PermissionUser = 0 + PermissionAdmin = 1 +) + const DeviceAID = 1 // TODO: fix someday +func GenerateKey() []byte { + _, key, _ := ed25519.GenerateKey(nil) + return key +} + func GenerateID(name string) string { sum := sha512.Sum512([]byte(name)) return fmt.Sprintf( @@ -28,46 +73,22 @@ func GenerateUUID() string { return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] } -type PairVerifyPayload struct { - Method byte `tlv8:"0,optional"` - Identifier string `tlv8:"1,optional"` - PublicKey []byte `tlv8:"3,optional"` - EncryptedData []byte `tlv8:"5,optional"` - State byte `tlv8:"6,optional"` - Status byte `tlv8:"7,optional"` - Signature []byte `tlv8:"10,optional"` +func Append(items ...any) (b []byte) { + for _, item := range items { + switch v := item.(type) { + case string: + b = append(b, v...) + case []byte: + b = append(b, v[:]...) + default: + panic(v) + } + } + return } -//func (c *Character) Unmarshal(value any) error { -// switch c.Format { -// case characteristic.FormatTLV8: -// data, err := base64.StdEncoding.DecodeString(c.Value.(string)) -// if err != nil { -// return err -// } -// return tlv8.Unmarshal(data, value) -// } -// return nil -//} - -//func (c *Character) Marshal(value any) error { -// switch c.Format { -// case characteristic.FormatTLV8: -// data, err := tlv8.Marshal(value) -// if err != nil { -// return err -// } -// c.Value = base64.StdEncoding.EncodeToString(data) -// } -// return nil -//} - -func (c *Character) String() string { - data, err := json.Marshal(c) - if err != nil { - return "ERROR" - } - return string(data) +func NewResponseError(req, res any) error { + return fmt.Errorf("hap: wrong response: %#v, on request: %#v", res, req) } func UnmarshalEvent(res *http.Response) (char *Character, err error) { diff --git a/pkg/hap/hkdf/hkdf.go b/pkg/hap/hkdf/hkdf.go new file mode 100644 index 00000000..989dcfb5 --- /dev/null +++ b/pkg/hap/hkdf/hkdf.go @@ -0,0 +1,17 @@ +package hkdf + +import ( + "crypto/sha512" + "io" + + "golang.org/x/crypto/hkdf" +) + +func Sha512(key []byte, salt, info string) ([]byte, error) { + r := hkdf.New(sha512.New, key, []byte(salt), []byte(info)) + + buf := make([]byte, 32) + _, err := io.ReadFull(r, buf) + + return buf, err +} diff --git a/pkg/hap/http.go b/pkg/hap/http.go deleted file mode 100644 index 4ef13d63..00000000 --- a/pkg/hap/http.go +++ /dev/null @@ -1,246 +0,0 @@ -package hap - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "net/http" - "net/textproto" - "strconv" -) - -const ( - MimeTLV8 = "application/pairing+tlv8" - MimeJSON = "application/hap+json" - - UriPairSetup = "/pair-setup" - UriPairVerify = "/pair-verify" - UriPairings = "/pairings" - UriAccessories = "/accessories" - UriCharacteristics = "/characteristics" - UriResource = "/resource" -) - -func (c *Conn) Write(p []byte) (r io.Reader, err error) { - if c.secure == nil { - if _, err = c.conn.Write(p); err == nil { - r = bufio.NewReader(c.conn) - } - } else { - if _, err = c.secure.Write(p); err == nil { - r = <-c.httpResponse - } - } - return -} - -func (c *Conn) Do(req *http.Request) (*http.Response, error) { - if c.secure == nil { - // insecure requests - if err := req.Write(c.conn); err != nil { - return nil, err - } - return http.ReadResponse(bufio.NewReader(c.conn), req) - } - - // secure support write interface to connection - if err := req.Write(c.secure); err != nil { - return nil, err - } - - // get decrypted buffer from connection - buf := <-c.httpResponse - - return http.ReadResponse(buf, req) -} - -func (c *Conn) Get(uri string) (*http.Response, error) { - req, err := http.NewRequest( - "GET", "http://"+c.DeviceAddress+uri, nil, - ) - if err != nil { - return nil, err - } - return c.Do(req) -} - -func (c *Conn) Post(uri string, data []byte) (*http.Response, error) { - req, err := http.NewRequest( - "POST", "http://"+c.DeviceAddress+uri, - bytes.NewReader(data), - ) - if err != nil { - return nil, err - } - - switch uri { - case "/pair-verify", "/pairings": - req.Header.Set("Content-Type", MimeTLV8) - case UriResource: - req.Header.Set("Content-Type", MimeJSON) - } - - return c.Do(req) -} - -func (c *Conn) Put(uri string, data []byte) (*http.Response, error) { - req, err := http.NewRequest( - "PUT", "http://"+c.DeviceAddress+uri, - bytes.NewReader(data), - ) - if err != nil { - return nil, err - } - - switch uri { - case UriCharacteristics: - req.Header.Set("Content-Type", MimeJSON) - } - - return c.Do(req) -} - -func (c *Conn) Handle() (err error) { - defer func() { - if c.conn == nil { - err = nil - } - }() - - b := make([]byte, 512000) - for { - var total, content int - header := -1 - - for { - var n1 int - n1, err = c.secure.Read(b[total:]) - if err != nil { - return err - } - - if n1 == 0 { - return io.EOF - } - - total += n1 - - // TODO: rewrite - if header == -1 { - // step 1. wait whole header - header = bytes.Index(b[:total], []byte("\r\n\r\n")) - if header < 0 { - continue - } - header += 4 - - // step 2. check content-length - i1 := bytes.Index(b[:total], []byte("Content-Length: ")) - if i1 < 0 { - break - } - i1 += 16 - i2 := bytes.IndexByte(b[i1:total], '\r') - content, err = strconv.Atoi(string(b[i1 : i1+i2])) - if err != nil { - break - } - } - - if total >= header+content { - break - } - } - - // copy slice to buffer - buf := bytes.NewBuffer(make([]byte, 0, total)) - buf.Write(b[:total]) - r := bufio.NewReader(buf) - - // EVENT/1.0 200 OK - if b[0] == 'E' { - if c.OnEvent == nil { - continue - } - - tp := textproto.NewReader(r) - - var s string - if s, err = tp.ReadLine(); err != nil { - return err - } - if s != "EVENT/1.0 200 OK" { - return errors.New("wrong response") - } - - var mimeHeader textproto.MIMEHeader - if mimeHeader, err = tp.ReadMIMEHeader(); err != nil { - return err - } - - var cl int - if cl, err = strconv.Atoi( - mimeHeader.Get("Content-Length"), - ); err != nil { - return err - } - - res := http.Response{ - StatusCode: 200, - Proto: "EVENT/1.0", - ProtoMajor: 1, - ProtoMinor: 0, - Header: http.Header(mimeHeader), - ContentLength: int64(cl), - Body: io.NopCloser(r), - } - c.OnEvent(&res) - continue - } - - //if bytes.Index(b, []byte("image/jpeg")) > 0 { - // if total, err = c.secure.Read(b); err != nil { - // return - // } - // buf.Write(b[:total]) - //} - - c.httpResponse <- r - } -} - -func WriteStatusCode(w io.Writer, statusCode int) (err error) { - body := []byte(fmt.Sprintf( - "HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode), - )) - //print("<<<", string(body), "<<<\n") - _, err = w.Write(body) - return -} - -func WriteResponse( - w io.Writer, statusCode int, contentType string, body []byte, -) (err error) { - header := fmt.Sprintf( - "HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n", - statusCode, http.StatusText(statusCode), contentType, len(body), - ) - body = append([]byte(header), body...) - //print("<<<", string(body), "<<<\n") - _, err = w.Write(body) - return -} - -func WriteChunked(w io.Writer, contentType string, body []byte) (err error) { - header := fmt.Sprintf( - "HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n", - contentType, len(body), - ) - body = append([]byte(header), body...) - body = append(body, "\n0\n\n"...) - //print("<<<", string(body), "<<<\n") - _, err = w.Write(body) - return -} diff --git a/pkg/hap/pairing.go b/pkg/hap/pairing.go deleted file mode 100644 index 7e8f5074..00000000 --- a/pkg/hap/pairing.go +++ /dev/null @@ -1,410 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/sha512" - "errors" - "github.com/brutella/hap" - "github.com/brutella/hap/chacha20poly1305" - "github.com/brutella/hap/curve25519" - "github.com/brutella/hap/ed25519" - "github.com/brutella/hap/hkdf" - "github.com/brutella/hap/tlv8" - "github.com/tadglines/go-pkgs/crypto/srp" - "net" - "net/http" -) - -type pairSetupPayload struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - Salt []byte `tlv8:"2"` - PublicKey []byte `tlv8:"3"` - Proof []byte `tlv8:"4"` - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - RetryDelay byte `tlv8:"8"` - Certificate []byte `tlv8:"9"` - Signature []byte `tlv8:"10"` - Permissions byte `tlv8:"11"` - FragmentData []byte `tlv8:"13"` - FragmentLast []byte `tlv8:"14"` -} - -func (s *Server) PairSetupHandler( - conn net.Conn, req *http.Request, -) (clientID string, err error) { - // STEP 1. Request from iPhone - payloadM1 := pairSetupPayload{} - if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil { - return - } - if payloadM1.State != hap.M1 { - err = errors.New("wrong state") - return - } - - // generate our session public and salt using PIN - username := []byte("Pair-Setup") - - var SRP *srp.SRP - if SRP, err = srp.NewSRP( - "rfc5054.3072", sha512.New, - keyDerivativeFuncRFC2945(username), - ); err != nil { - return - } - - SRP.SaltLength = 16 - var salt, verifier []byte - if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil { - return - } - session := SRP.NewServerSession(username, salt, verifier) - - // STEP 2. Response to iPhone - payloadM2 := struct { - Salt []byte `tlv8:"2"` - PublicKey []byte `tlv8:"3"` - State byte `tlv8:"6"` - }{ - State: hap.M2, - PublicKey: session.GetB(), - Salt: salt, - } - var buf []byte - if buf, err = tlv8.Marshal(payloadM2); err != nil { - return - } - if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { - return - } - - // STEP 3. Request from iPhone - r := bufio.NewReader(conn) - if req, err = http.ReadRequest(r); err != nil { - return - } - payloadM3 := pairSetupPayload{} - if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil { - return - } - if payloadM3.State != hap.M3 { - err = errors.New("wrong state") - return - } - - // important to compute key before verify client - var sessionShared []byte - if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil { - return - } - - // support skip pin verify (any pin accepted) - if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) { - err = errors.New("client proof is invalid") - return - } - - serverProof := session.ComputeAuthenticator(payloadM3.Proof) - - // STEP 4. Response to iPhone - payloadM4 := struct { - Proof []byte `tlv8:"4"` - State byte `tlv8:"6"` - }{ - State: hap.M4, Proof: serverProof, - } - if buf, err = tlv8.Marshal(payloadM4); err != nil { - return - } - if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { - return - } - - // STEP 5. Request from iPhone - if req, err = http.ReadRequest(r); err != nil { - return - } - encryptedM5 := pairSetupPayload{} - if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil { - return - } - if encryptedM5.State != hap.M5 { - err = errors.New("wrong state") - return - } - - msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16] - var mac [16]byte - copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC) - - // decrypt message using session shared - var sessionKey [32]byte - if sessionKey, err = hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Encrypt-Salt"), - []byte("Pair-Setup-Encrypt-Info"), - ); err != nil { - return - } - - if buf, err = chacha20poly1305.DecryptAndVerify( - sessionKey[:], []byte("PS-Msg05"), msg, mac, nil, - ); err != nil { - return - } - - // unpack message from TLV8 - payloadM5 := struct { - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` - }{} - if err = tlv8.Unmarshal(buf, &payloadM5); err != nil { - return - } - - // 3. verify client ID and Public - var saltKey [32]byte - if saltKey, err = hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"), - []byte("Pair-Setup-Controller-Sign-Info"), - ); err != nil { - return - } - - buf = nil - buf = append(buf, saltKey[:]...) - buf = append(buf, payloadM5.Identifier...) - buf = append(buf, payloadM5.PublicKey[:]...) - - if !ed25519.ValidateSignature( - payloadM5.PublicKey[:], buf, payloadM5.Signature, - ) { - err = errors.New("wrong client signature") - return - } - - // 4. generate signature to our ID adn Public - if saltKey, err = hkdf.Sha512( - sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"), - []byte("Pair-Setup-Accessory-Sign-Info"), - ); err != nil { - return - } - - buf = nil - buf = append(buf, saltKey[:]...) - buf = append(buf, []byte(s.ServerID)...) - buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic - - var signature []byte - if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil { - return - } - - // 5. pack our ID and Public - payloadM6 := struct { - Identifier []byte `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` - }{ - Identifier: []byte(s.ServerID), - PublicKey: s.ServerPrivate[32:], - Signature: signature, - } - if buf, err = tlv8.Marshal(payloadM6); err != nil { - return - } - - // 6. encrypt message - buf, mac, _ = chacha20poly1305.EncryptAndSeal( - sessionKey[:], []byte("PS-Msg06"), buf, nil, - ) - - // STEP 6. Response to iPhone - encryptedM6 := struct { - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - }{ - State: hap.M6, - EncryptedData: append(buf, mac[:]...), - } - if buf, err = tlv8.Marshal(encryptedM6); err != nil { - return - } - if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { - return - } - - if s.Pairings != nil { - s.Pairings[payloadM5.Identifier] = append( - payloadM5.PublicKey, 1, // adds admin (1) flag - ) - } - - clientID = payloadM5.Identifier - - return -} - -func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc { - return func(salt, pin []byte) []byte { - h := sha512.New() - h.Write(username) - h.Write([]byte(":")) - h.Write(pin) - t2 := h.Sum(nil) - h.Reset() - h.Write(salt) - h.Write(t2) - return h.Sum(nil) - } -} - -type pairVerifyPayload struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - Signature []byte `tlv8:"10"` -} - -func (s *Server) PairVerifyHandler( - conn net.Conn, req *http.Request, -) (secure *Secure, err error) { - // STEP M1. Request from iPhone - payloadM1 := pairVerifyPayload{} - if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil { - return - } - if payloadM1.State != hap.M1 { - err = errors.New("wrong state") - return - } - - var clientPublic [32]byte - copy(clientPublic[:], payloadM1.PublicKey) - - // Generate the key pair. - sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() - sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic) - - var sessionKey [32]byte - if sessionKey, err = hkdf.Sha512( - sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"), - []byte("Pair-Verify-Encrypt-Info"), - ); err != nil { - return - } - - var buf []byte - buf = append(buf, sessionPublic[:]...) - buf = append(buf, s.ServerID...) - buf = append(buf, clientPublic[:]...) - - var signature []byte - if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil { - return - } - - // STEP M2. Response to iPhone - payloadM2 := struct { - Identifier string `tlv8:"1"` - Signature []byte `tlv8:"10"` - }{ - Identifier: s.ServerID, - Signature: signature, - } - if buf, err = tlv8.Marshal(payloadM2); err != nil { - return - } - - var mac [16]byte - buf, mac, _ = chacha20poly1305.EncryptAndSeal( - sessionKey[:], []byte("PV-Msg02"), buf, nil, - ) - encryptedM2 := struct { - State byte `tlv8:"6"` - PublicKey []byte `tlv8:"3"` - EncryptedData []byte `tlv8:"5"` - }{ - State: hap.M2, - PublicKey: sessionPublic[:], - EncryptedData: append(buf, mac[:]...), - } - if buf, err = tlv8.Marshal(encryptedM2); err != nil { - return - } - if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { - return - } - - // STEP M3. Request from iPhone - r := bufio.NewReader(conn) - if req, err = http.ReadRequest(r); err != nil { - return - } - encryptedM3 := pairSetupPayload{} - if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil { - return - } - if encryptedM3.State != hap.M3 { - err = errors.New("wrong state") - return - } - - buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16] - copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC) - - if buf, err = chacha20poly1305.DecryptAndVerify( - sessionKey[:], []byte("PV-Msg03"), buf, mac, nil, - ); err != nil { - return - } - - payloadM3 := pairVerifyPayload{} - if err = tlv8.Unmarshal(buf, &payloadM3); err != nil { - return - } - - if s.Pairings != nil { - pairing := s.Pairings[payloadM3.Identifier] - if pairing == nil { - err = errors.New("not paired yet") - return - } - - buf = nil - buf = append(buf, clientPublic[:]...) - buf = append(buf, []byte(payloadM3.Identifier)...) - buf = append(buf, sessionPublic[:]...) - - if !ed25519.ValidateSignature( - pairing[:32], buf, payloadM3.Signature, - ) { - err = errors.New("signature invalid") - return - } - } - - // STEP M4. Response to iPhone - payloadM4 := struct { - State byte `tlv8:"6"` - }{ - State: hap.M4, - } - if buf, err = tlv8.Marshal(payloadM4); err != nil { - return - } - err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf) - - if secure, err = NewSecure(sessionShared, true); err != nil { - return - } - secure.Conn = conn - - return -} diff --git a/pkg/hap/secure.go b/pkg/hap/secure.go deleted file mode 100644 index 3990196c..00000000 --- a/pkg/hap/secure.go +++ /dev/null @@ -1,137 +0,0 @@ -package hap - -import ( - "encoding/binary" - "github.com/brutella/hap/chacha20poly1305" - "github.com/brutella/hap/hkdf" - "net" - "sync" -) - -type Secure struct { - Conn net.Conn - - encryptKey [32]byte - decryptKey [32]byte - encryptCount uint64 - decryptCount uint64 - - mx sync.Mutex -} - -func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) { - salt := []byte("Control-Salt") - - key1, err := hkdf.Sha512( - sharedKey[:], salt, []byte("Control-Read-Encryption-Key"), - ) - if err != nil { - return nil, err - } - - key2, err := hkdf.Sha512( - sharedKey[:], salt, []byte("Control-Write-Encryption-Key"), - ) - if err != nil { - return nil, err - } - - if isServer { - return &Secure{encryptKey: key1, decryptKey: key2}, nil - } else { - return &Secure{encryptKey: key2, decryptKey: key1}, nil - } -} - -func (s *Secure) Read(b []byte) (n int, err error) { - for { - var length uint16 - if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil { - return - } - - var enc = make([]byte, length) - if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil { - return - } - - var mac [16]byte - if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil { - return - } - - var nonce [8]byte - binary.LittleEndian.PutUint64(nonce[:], s.decryptCount) - s.decryptCount++ - - bLength := make([]byte, 2) - binary.LittleEndian.PutUint16(bLength, length) - - var msg []byte - if msg, err = chacha20poly1305.DecryptAndVerify( - s.decryptKey[:], nonce[:], enc, mac, bLength, - ); err != nil { - return - } - - n += copy(b[n:], msg) - - // Finish when all bytes fit in b - if length < packetLengthMax { - //fmt.Printf(">>>%s>>>\n", b[:n]) - return - } - } -} - -func (s *Secure) Write(b []byte) (n int, err error) { - s.mx.Lock() - defer s.mx.Unlock() - - var packetLen = len(b) - for { - if packetLen > packetLengthMax { - packetLen = packetLengthMax - } - - //fmt.Printf("<<<%s<<<\n", b[:packetLen]) - - var nonce [8]byte - binary.LittleEndian.PutUint64(nonce[:], s.encryptCount) - s.encryptCount++ - - bLength := make([]byte, 2) - binary.LittleEndian.PutUint16(bLength, uint16(packetLen)) - - var enc []byte - var mac [16]byte - enc, mac, err = chacha20poly1305.EncryptAndSeal( - s.encryptKey[:], nonce[:], b[:packetLen], bLength[:], - ) - if err != nil { - return - } - - enc = append(bLength, enc...) - enc = append(enc, mac[:]...) - if _, err = s.Conn.Write(enc); err != nil { - return - } - - n += packetLen - - if packetLen == packetLengthMax { - b = b[packetLengthMax:] - packetLen = len(b) - } else { - break - } - } - - return -} - -const ( - // packetLengthMax is the max length of encrypted packets - packetLengthMax = 0x400 -) diff --git a/pkg/hap/secure/secure.go b/pkg/hap/secure/secure.go new file mode 100644 index 00000000..29c17ce6 --- /dev/null +++ b/pkg/hap/secure/secure.go @@ -0,0 +1,156 @@ +package secure + +import ( + "encoding/binary" + "io" + "net" + "sync" + "time" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" +) + +type Conn struct { + conn net.Conn + + encryptKey []byte + decryptKey []byte + encryptCnt uint64 + decryptCnt uint64 + + mx sync.Mutex +} + +func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { + key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key") + if err != nil { + return nil, err + } + + key2, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Write-Encryption-Key") + if err != nil { + return nil, err + } + + if isClient { + return &Conn{conn: conn, encryptKey: key2, decryptKey: key1}, nil + } else { + return &Conn{conn: conn, encryptKey: key1, decryptKey: key2}, nil + } +} + +const ( + // PacketSizeMax is the max length of encrypted packets + PacketSizeMax = 0x400 + + VerifySize = 2 + NonceSize = 8 + Overhead = 16 // chacha20poly1305.Overhead +) + +func (c *Conn) Read(b []byte) (n int, err error) { + verify := make([]byte, VerifySize) // = packet length + buf := make([]byte, PacketSizeMax+Overhead) + nonce := make([]byte, NonceSize) + + for { + if len(b) < PacketSizeMax { + return + } + + if _, err = io.ReadFull(c.conn, verify); err != nil { + return + } + + size := binary.LittleEndian.Uint16(verify) + ciphertext := buf[:size+Overhead] + + if _, err = io.ReadFull(c.conn, ciphertext); err != nil { + return + } + + binary.LittleEndian.PutUint64(nonce, c.decryptCnt) + c.decryptCnt++ + + // put decrypted text to b's end + _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify) + if err != nil { + return + } + + n += int(size) // plaintext size + + // Finish when all bytes fit in b + if size < PacketSizeMax { + return + } + + b = b[size:] + } +} + +func (c *Conn) Write(b []byte) (n int, err error) { + c.mx.Lock() + defer c.mx.Unlock() + + nonce := make([]byte, NonceSize) + buf := make([]byte, NonceSize+PacketSizeMax+Overhead) + verify := buf[:VerifySize] // part of write buffer + + for { + size := len(b) + if size > PacketSizeMax { + size = PacketSizeMax + } + + binary.LittleEndian.PutUint16(verify, uint16(size)) + + binary.LittleEndian.PutUint64(nonce, c.encryptCnt) + c.encryptCnt++ + + // put encrypted text to writing buffer just after size (2 bytes) + _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[2:2], nonce, b[:size], verify) + if err != nil { + return + } + + if _, err = c.conn.Write(buf[:VerifySize+size+Overhead]); err != nil { + return + } + + n += size // plaintext size + + if size < PacketSizeMax { + break + } + + b = b[PacketSizeMax:] + } + + 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/server.go b/pkg/hap/server.go deleted file mode 100644 index 460012fc..00000000 --- a/pkg/hap/server.go +++ /dev/null @@ -1,155 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/ed25519" - "github.com/brutella/hap" - "github.com/brutella/hap/tlv8" - "io" - "net" - "net/http" -) - -type Server struct { - // Pin can't be null because server proof will be wrong - Pin string `json:"-"` - - ServerID string `json:"server_id"` - // 32 bytes private key + 32 bytes public key - ServerPrivate []byte `json:"server_private"` - - // Pairings can be nil for disable pair verify check - // ClientID: 32 bytes client public + 1 byte (isAdmin) - Pairings map[string][]byte `json:"pairings"` - - DefaultPlainHandler func(w io.Writer, r *http.Request) error - DefaultSecureHandler func(w io.Writer, r *http.Request) error - - OnPairChange func(clientID string, clientPublic []byte) `json:"-"` - OnRequest func(w io.Writer, r *http.Request) `json:"-"` -} - -func GenerateKey() []byte { - _, key, _ := ed25519.GenerateKey(nil) - return key -} - -func NewServer(name string) *Server { - return &Server{ - ServerID: GenerateID(name), - ServerPrivate: GenerateKey(), - Pairings: map[string][]byte{}, - } -} - -func (s *Server) Serve(address string) (err error) { - var ln net.Listener - if ln, err = net.Listen("tcp4", address); err != nil { - return - } - - for { - var conn net.Conn - if conn, err = ln.Accept(); err != nil { - continue - } - go func() { - //fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String()) - s.Accept(conn) - //fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String()) - }() - } -} - -func (s *Server) Accept(conn net.Conn) (err error) { - defer conn.Close() - - var req *http.Request - r := bufio.NewReader(conn) - if req, err = http.ReadRequest(r); err != nil { - return - } - - return s.HandleRequest(conn, req) -} - -func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) { - if s.OnRequest != nil { - s.OnRequest(conn, req) - } - - switch req.URL.Path { - case UriPairSetup: - if _, err = s.PairSetupHandler(conn, req); err != nil { - return - } - - case UriPairVerify: - var secure *Secure - if secure, err = s.PairVerifyHandler(conn, req); err != nil { - return - } - - err = s.HandleSecure(secure) - - default: - if s.DefaultPlainHandler != nil { - err = s.DefaultPlainHandler(conn, req) - } - } - - return -} - -func (s *Server) HandleSecure(secure *Secure) (err error) { - r := bufio.NewReader(secure) - for { - var req *http.Request - if req, err = http.ReadRequest(r); err != nil { - return - } - - if s.OnRequest != nil { - s.OnRequest(secure, req) - } - - switch req.URL.Path { - case UriPairings: - s.HandlePairings(secure, req) - default: - if err = s.DefaultSecureHandler(secure, req); err != nil { - return - } - } - } -} - -func (s *Server) HandlePairings(w io.Writer, r *http.Request) { - req := struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Permission byte `tlv8:"11"` - State byte `tlv8:"6"` - }{} - - if err := tlv8.UnmarshalReader(r.Body, &req); err != nil { - panic(err) - } - - switch req.Method { - case hap.MethodAddPairing, hap.MethodDeletePairing: - res := struct { - State byte `tlv8:"6"` - }{ - State: hap.M2, - } - data, err := tlv8.Marshal(res) - if err != nil { - panic(err) - } - if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil { - panic(err) - } - } -} diff --git a/pkg/hap/tlv8/tlv8.go b/pkg/hap/tlv8/tlv8.go new file mode 100644 index 00000000..69edb9ee --- /dev/null +++ b/pkg/hap/tlv8/tlv8.go @@ -0,0 +1,333 @@ +package tlv8 + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "reflect" + "strconv" +) + +type errReader struct { + err error +} + +func (e *errReader) Read([]byte) (int, error) { + return 0, e.err +} + +func MarshalBase64(v any) (string, error) { + b, err := Marshal(v) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(b), nil +} + +func MarshalReader(v any) io.Reader { + b, err := Marshal(v) + if err != nil { + return &errReader{err: err} + } + return bytes.NewReader(b) +} + +func Marshal(v any) ([]byte, error) { + value := reflect.ValueOf(v) + kind := value.Type().Kind() + + if kind == reflect.Pointer { + value = value.Elem() + kind = value.Type().Kind() + } + + switch kind { + case reflect.Struct: + return appendStruct(nil, value) + } + + return nil, errors.New("tlv8: not implemented: " + kind.String()) +} + +func appendStruct(b []byte, value reflect.Value) ([]byte, error) { + valueType := value.Type() + + for i := 0; i < value.NumField(); i++ { + refField := value.Field(i) + s, ok := valueType.Field(i).Tag.Lookup("tlv8") + if !ok { + continue + } + + tag, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + + b, err = appendValue(b, byte(tag), refField) + if err != nil { + return nil, err + } + } + + return b, nil +} + +func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { + var err error + + switch value.Kind() { + case reflect.Uint8: + v := value.Uint() + return append(b, tag, 1, byte(v)), nil + + case reflect.Int8: + v := value.Int() + return append(b, tag, 1, byte(v)), nil + + case reflect.Uint16: + v := value.Uint() + return append(b, tag, 2, byte(v), byte(v>>8)), nil + + case reflect.Uint32: + v := value.Uint() + return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil + + case reflect.Float32: + v := math.Float32bits(float32(value.Float())) + return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil + + case reflect.String: + v := value.String() + b = append(b, tag, byte(len(v))) + return append(b, v...), nil + + case reflect.Array: + if value.Type().Elem().Kind() == reflect.Uint8 { + n := value.Len() + b = append(b, tag, byte(n)) + for i := 0; i < n; i++ { + b = append(b, byte(value.Index(i).Uint())) + } + return b, nil + } + + case reflect.Slice: + // byte array + if value.Type().Elem().Kind() == reflect.Uint8 { + v := value.Bytes() + l := len(v) + for ; l > 255; l -= 255 { + b = append(b, tag, 255) + b = append(b, v[:255]...) + v = v[255:] + } + b = append(b, tag, byte(l)) + return append(b, v...), nil + } + + for i := 0; i < value.Len(); i++ { + if i > 0 { + b = append(b, 0, 0) + } + if b, err = appendValue(b, tag, value.Index(i)); err != nil { + return nil, err + } + } + return b, nil + + case reflect.Struct: + b = append(b, tag, 0) + i := len(b) + if b, err = appendStruct(b, value); err != nil { + return nil, err + } + b[i-1] = byte(len(b) - i) // set struct size + return b, nil + } + + return nil, errors.New("tlv8: not implemented: " + value.Kind().String()) +} + +func UnmarshalBase64(s string, v any) error { + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + return Unmarshal(data, v) +} + +func UnmarshalReader(r io.Reader, v any) error { + data, err := io.ReadAll(r) + if err != nil { + return err + } + return Unmarshal(data, v) +} + +func Unmarshal(data []byte, v any) error { + if len(data) == 0 { + return errors.New("tlv8: unmarshal zero data") + } + + value := reflect.ValueOf(v) + kind := value.Type().Kind() + + if kind != reflect.Pointer { + return errors.New("tlv8: value should be pointer: " + kind.String()) + } + + value = value.Elem() + kind = value.Type().Kind() + + switch kind { + case reflect.Struct: + return unmarshalStruct(data, value) + } + + return errors.New("tlv8: not implemented: " + kind.String()) +} + +func unmarshalStruct(b []byte, value reflect.Value) error { + for len(b) >= 2 { + t := b[0] + l := int(b[1]) + + // array item divider + if t == 0 && l == 0 { + b = b[2:] + continue + } + + var v []byte + + for { + if len(b) < 2+l { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + + v = append(v, b[2:2+l]...) + b = b[2+l:] + + // if size == 255 and same tag - continue read big payload + if l < 255 || len(b) < 2 || b[0] != t { + break + } + + l = int(b[1]) + } + + tag := strconv.Itoa(int(t)) + + valueField, ok := getStructField(value, tag) + if !ok { + return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name()) + } + + if err := unmarshalValue(v, valueField); err != nil { + return err + } + } + + return nil +} + +func unmarshalValue(v []byte, value reflect.Value) error { + switch value.Kind() { + case reflect.Uint8: + if len(v) != 1 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetUint(uint64(v[0])) + + case reflect.Int8: + if len(v) != 1 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetInt(int64(v[0])) + + case reflect.Uint16: + if len(v) != 2 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetUint(uint64(v[0]) | uint64(v[1])<<8) + + case reflect.Uint32: + if len(v) != 4 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24) + + case reflect.Float32: + f := math.Float32frombits(binary.LittleEndian.Uint32(v)) + value.SetFloat(float64(f)) + + case reflect.String: + value.SetString(string(v)) + + case reflect.Array: + if kind := value.Type().Elem().Kind(); kind != reflect.Uint8 { + return errors.New("tlv8: unsupported array: " + kind.String()) + } + + for i, b := range v { + value.Index(i).SetUint(uint64(b)) + } + return nil + + case reflect.Slice: + if value.Type().Elem().Kind() == reflect.Uint8 { + value.SetBytes(v) + return nil + } + + i := growSlice(value) + return unmarshalValue(v, value.Index(i)) + + case reflect.Struct: + return unmarshalStruct(v, value) + + default: + return errors.New("tlv8: not implemented: " + value.Kind().String()) + } + + return nil +} + +func getStructField(value reflect.Value, tag string) (reflect.Value, bool) { + valueType := value.Type() + + for i := 0; i < value.NumField(); i++ { + valueField := value.Field(i) + + if s, ok := valueType.Field(i).Tag.Lookup("tlv8"); ok && s == tag { + return valueField, true + } + } + + return reflect.Value{}, false +} + +func growSlice(value reflect.Value) int { + size := value.Len() + + if size >= value.Cap() { + newcap := value.Cap() + value.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newValue := reflect.MakeSlice(value.Type(), value.Len(), newcap) + reflect.Copy(newValue, value) + value.Set(newValue) + } + + if size >= value.Len() { + value.SetLen(size + 1) + } + + return size +} diff --git a/pkg/hap/tlv8/tlv8_test.go b/pkg/hap/tlv8/tlv8_test.go new file mode 100644 index 00000000..0cab43bc --- /dev/null +++ b/pkg/hap/tlv8/tlv8_test.go @@ -0,0 +1,38 @@ +package tlv8 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshal(t *testing.T) { + type Struct struct { + Byte byte `tlv8:"1"` + Uint16 uint16 `tlv8:"2"` + Uint32 uint32 `tlv8:"3"` + Float32 float32 `tlv8:"4"` + String string `tlv8:"5"` + Slice []byte `tlv8:"6"` + Array [4]byte `tlv8:"7"` + } + + src := Struct{ + Byte: 1, + Uint16: 2, + Uint32: 3, + Float32: 1.23, + String: "123", + Slice: []byte{1, 2, 3}, + Array: [4]byte{1, 2, 3, 4}, + } + + b, err := Marshal(src) + require.Nil(t, err) + + var dst Struct + err = Unmarshal(b, &dst) + require.Nil(t, err) + + require.Equal(t, src, dst) +} diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index 3314efaf..f0769a39 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -4,24 +4,24 @@ import ( "encoding/json" "errors" "fmt" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/hap" - "github.com/AlexxIT/go2rtc/pkg/hap/camera" - "github.com/AlexxIT/go2rtc/pkg/srtp" - "github.com/brutella/hap/characteristic" - "github.com/brutella/hap/rtp" "net" "net/url" "sync/atomic" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/srtp" ) type Client struct { core.Listener - conn *hap.Conn - exit chan error + conn *hap.Client server *srtp.Server - url string + config *StreamConfig medias []*core.Media receivers []*core.Receiver @@ -29,6 +29,11 @@ type Client struct { sessions []*srtp.Session } +type StreamConfig struct { + Video camera.SupportedVideoStreamConfig + Audio camera.SupportedAudioStreamConfig +} + func NewClient(rawURL string, server *srtp.Server) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { @@ -36,7 +41,7 @@ func NewClient(rawURL string, server *srtp.Server) (*Client, error) { } query := u.Query() - c := &hap.Conn{ + c := &hap.Client{ DeviceAddress: u.Host, DeviceID: query.Get("device_id"), DevicePublic: hap.DecodeKey(query.Get("device_public")), @@ -48,23 +53,131 @@ func NewClient(rawURL string, server *srtp.Server) (*Client, error) { } func (c *Client) Dial() error { - if err := c.conn.Dial(); err != nil { - return err - } - - c.exit = make(chan error) - - go func() { - //start goroutine for reading responses from camera - c.exit <- c.conn.Handle() - }() - - return nil + return c.conn.Dial() } func (c *Client) GetMedias() []*core.Media { - if c.medias == nil { - c.medias = c.getMedias() + if c.medias != nil { + return c.medias + } + + accs, err := c.conn.GetAccessories() + if err != nil { + return nil + } + + acc := accs[0] + + c.config = &StreamConfig{} + + // get supported video config (not really necessary) + char := acc.GetCharacter(camera.TypeSupportedVideoStreamConfiguration) + if char == nil { + return nil + } + if err = char.ReadTLV8(&c.config.Video); err != nil { + return nil + } + + for _, videoCodec := range c.config.Video.Codecs { + var name string + + switch videoCodec.CodecType { + case camera.VideoCodecTypeH264: + name = core.CodecH264 + default: + continue + } + + for _, params := range videoCodec.CodecParams { + codec := &core.Codec{ + Name: name, + ClockRate: 90000, + FmtpLine: "profile-level-id=", + } + + switch params.ProfileID { + case camera.VideoCodecProfileConstrainedBaseline: + codec.FmtpLine += "4200" // 4240? + case camera.VideoCodecProfileMain: + codec.FmtpLine += "4D00" // 4D40? + case camera.VideoCodecProfileHigh: + codec.FmtpLine += "6400" + default: + continue + } + + switch params.Level { + case camera.VideoCodecLevel31: + codec.FmtpLine += "1F" + case camera.VideoCodecLevel32: + codec.FmtpLine += "20" + case camera.VideoCodecLevel40: + codec.FmtpLine += "28" + default: + continue + } + + media := &core.Media{ + Kind: core.KindVideo, Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + } + c.medias = append(c.medias, media) + } + } + + char = acc.GetCharacter(camera.TypeSupportedAudioStreamConfiguration) + if char == nil { + return nil + } + if err = char.ReadTLV8(&c.config.Audio); err != nil { + return nil + } + + for _, audioCodec := range c.config.Audio.Codecs { + var name string + + switch audioCodec.CodecType { + case camera.AudioCodecTypePCMU: + name = core.CodecPCMU + case camera.AudioCodecTypePCMA: + name = core.CodecPCMA + case camera.AudioCodecTypeAACELD: + name = core.CodecELD + case camera.AudioCodecTypeOpus: + name = core.CodecOpus + default: + continue + } + + for _, params := range audioCodec.CodecParams { + codec := &core.Codec{ + Name: name, + Channels: uint16(params.Channels), + } + + if name == core.CodecELD { + // only this value supported by FFmpeg + codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000" + } + + switch params.SampleRate { + case camera.AudioCodecSampleRate8Khz: + codec.ClockRate = 8000 + case camera.AudioCodecSampleRate16Khz: + codec.ClockRate = 16000 + case camera.AudioCodecSampleRate24Khz: + codec.ClockRate = 24000 + default: + continue + } + + media := &core.Media{ + Kind: core.KindAudio, Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + } + c.medias = append(c.medias, media) + } } return c.medias @@ -93,177 +206,142 @@ func (c *Client) Start() error { return err } - // TODO: set right config - vp := &rtp.VideoParameters{ - CodecType: rtp.VideoCodecType_H264, - CodecParams: rtp.VideoCodecParameters{ - Profiles: []rtp.VideoCodecProfile{ - {Id: rtp.VideoCodecProfileMain}, - }, - Levels: []rtp.VideoCodecLevel{ - {Level: rtp.VideoCodecLevel4}, - }, - Packetizations: []rtp.VideoCodecPacketization{ - {Mode: rtp.VideoCodecPacketizationModeNonInterleaved}, - }, - }, - Attributes: rtp.VideoCodecAttributes{ + videoParams := &camera.SelectedVideoParams{ + CodecType: camera.VideoCodecTypeH264, + VideoAttrs: camera.VideoAttrs{ Width: 1920, Height: 1080, Framerate: 30, }, } - ap := &rtp.AudioParameters{ - CodecType: rtp.AudioCodecType_AAC_ELD, - CodecParams: rtp.AudioCodecParameters{ - Channels: 1, - Bitrate: rtp.AudioCodecBitrateVariable, - Samplerate: rtp.AudioCodecSampleRate16Khz, - // packet time=20 => AAC-ELD packet size=480 - // packet time=30 => AAC-ELD packet size=480 - // packet time=40 => AAC-ELD packet size=480 - // packet time=60 => AAC-LD packet size=960 - PacketTime: 40, + videoTrack := c.trackByKind(core.KindVideo) + if videoTrack != nil { + profile := h264.GetProfileLevelID(videoTrack.Codec.FmtpLine) + + switch profile[:2] { + case "42": + videoParams.CodecParams.ProfileID = camera.VideoCodecProfileConstrainedBaseline + case "4D": + videoParams.CodecParams.ProfileID = camera.VideoCodecProfileMain + case "64": + videoParams.CodecParams.ProfileID = camera.VideoCodecProfileHigh + } + + switch profile[4:] { + case "1F": + videoParams.CodecParams.Level = camera.VideoCodecLevel31 + case "20": + videoParams.CodecParams.Level = camera.VideoCodecLevel32 + case "28": + videoParams.CodecParams.Level = camera.VideoCodecLevel40 + } + } else { + // if consumer don't need track - ask first track from camera + codec0 := c.config.Video.Codecs[0] + videoParams.CodecParams.ProfileID = codec0.CodecParams[0].ProfileID + videoParams.CodecParams.Level = codec0.CodecParams[0].Level + } + + audioParams := &camera.SelectedAudioParams{ + CodecParams: camera.AudioCodecParams{ + Bitrate: camera.AudioCodecBitrateVariable, + // RTPTime=20 => AAC-ELD packet size=480 + // RTPTime=30 => AAC-ELD packet size=480 + // RTPTime=40 => AAC-ELD packet size=480 + // RTPTime=60 => AAC-LD packet size=960 + RTPTime: 40, }, } + audioTrack := c.trackByKind(core.KindAudio) + if audioTrack != nil { + audioParams.CodecParams.Channels = byte(audioTrack.Codec.Channels) + + switch audioTrack.Codec.Name { + case core.CodecPCMU: + audioParams.CodecType = camera.AudioCodecTypePCMU + case core.CodecPCMA: + audioParams.CodecType = camera.AudioCodecTypePCMA + case core.CodecELD: + audioParams.CodecType = camera.AudioCodecTypeAACELD + case core.CodecOpus: + audioParams.CodecType = camera.AudioCodecTypeOpus + } + + switch audioTrack.Codec.ClockRate { + case 8000: + audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate8Khz + case 16000: + audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate16Khz + case 24000: + audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate24Khz + } + } else { + // if consumer don't need track - ask first track from camera + codec0 := c.config.Audio.Codecs[0] + audioParams.CodecType = codec0.CodecType + audioParams.CodecParams.Channels = codec0.CodecParams[0].Channels + audioParams.CodecParams.SampleRate = codec0.CodecParams[0].SampleRate + } + // setup HomeKit stream session - hkSession := camera.NewSession(vp, ap) - hkSession.SetLocalEndpoint(host, c.server.Port()) + session := camera.NewSession(videoParams, audioParams) + session.SetLocalEndpoint(host, c.server.Port()) // create client for processing camera accessory cam := camera.NewClient(c.conn) // try to start HomeKit stream - if err = cam.StartStream(hkSession); err != nil { + if err = cam.StartStream(session); err != nil { return err } // SRTP Video Session - vs := &srtp.Session{ - LocalSSRC: hkSession.Config.Video.RTP.Ssrc, - RemoteSSRC: hkSession.Answer.SsrcVideo, + videoSession := &srtp.Session{ + LocalSSRC: session.Config.VideoParams.RTPParams.SSRC, + RemoteSSRC: session.Answer.VideoSSRC, + Track: videoTrack, } - if err = vs.SetKeys( - hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt, - hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt, + if err = videoSession.SetKeys( + session.Offer.VideoCrypto.MasterKey, session.Offer.VideoCrypto.MasterSalt, + session.Answer.VideoCrypto.MasterKey, session.Answer.VideoCrypto.MasterSalt, ); err != nil { return err } // SRTP Audio Session - as := &srtp.Session{ - LocalSSRC: hkSession.Config.Audio.RTP.Ssrc, - RemoteSSRC: hkSession.Answer.SsrcAudio, + audioSession := &srtp.Session{ + LocalSSRC: session.Config.AudioParams.RTPParams.SSRC, + RemoteSSRC: session.Answer.AudioSSRC, + Track: audioTrack, } - if err = as.SetKeys( - hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt, - hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt, + if err = audioSession.SetKeys( + session.Offer.AudioCrypto.MasterKey, session.Offer.AudioCrypto.MasterSalt, + session.Answer.AudioCrypto.MasterKey, session.Answer.AudioCrypto.MasterSalt, ); err != nil { return err } - for _, track := range c.receivers { - switch track.Codec.Name { - case core.CodecH264: - vs.Track = track - case core.CodecELD: - as.Track = track - } + c.server.AddSession(videoSession) + c.server.AddSession(audioSession) + + c.sessions = []*srtp.Session{videoSession, audioSession} + + if audioSession.Track != nil { + audioSession.Deadline = time.NewTimer(core.ConnDeadline) + <-audioSession.Deadline.C + } else if videoSession.Track != nil { + videoSession.Deadline = time.NewTimer(core.ConnDeadline) + <-videoSession.Deadline.C } - c.server.AddSession(vs) - c.server.AddSession(as) - - c.sessions = []*srtp.Session{vs, as} - - return <-c.exit + return nil } func (c *Client) Stop() error { - err := c.conn.Close() - for _, session := range c.sessions { c.server.RemoveSession(session) } - return err -} - -func (c *Client) getMedias() []*core.Media { - var medias []*core.Media - - accs, err := c.conn.GetAccessories() - if err != nil { - return nil - } - - acc := accs[0] - - // get supported video config (not really necessary) - char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration) - v1 := &rtp.VideoStreamConfiguration{} - if err = char.ReadTLV8(v1); err != nil { - return nil - } - - for _, hkCodec := range v1.Codecs { - codec := &core.Codec{ClockRate: 90000} - - switch hkCodec.Type { - case rtp.VideoCodecType_H264: - codec.Name = core.CodecH264 - codec.FmtpLine = "profile-level-id=420029" - default: - fmt.Printf("unknown codec: %d", hkCodec.Type) - continue - } - - media := &core.Media{ - Kind: core.KindVideo, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, - } - medias = append(medias, media) - } - - char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration) - v2 := &rtp.AudioStreamConfiguration{} - if err = char.ReadTLV8(v2); err != nil { - return nil - } - - for _, hkCodec := range v2.Codecs { - codec := &core.Codec{ - Channels: uint16(hkCodec.Parameters.Channels), - } - - switch hkCodec.Parameters.Samplerate { - case rtp.AudioCodecSampleRate8Khz: - codec.ClockRate = 8000 - case rtp.AudioCodecSampleRate16Khz: - codec.ClockRate = 16000 - case rtp.AudioCodecSampleRate24Khz: - codec.ClockRate = 24000 - default: - panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate)) - } - - switch hkCodec.Type { - case rtp.AudioCodecType_AAC_ELD: - codec.Name = core.CodecELD - // only this value supported by FFmpeg - codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000" - default: - fmt.Printf("unknown codec: %d", hkCodec.Type) - continue - } - - media := &core.Media{ - Kind: core.KindAudio, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, - } - medias = append(medias, media) - } - - return medias + return c.conn.Close() } func (c *Client) MarshalJSON() ([]byte, error) { @@ -275,9 +353,19 @@ func (c *Client) MarshalJSON() ([]byte, error) { info := &core.Info{ Type: "HomeKit active producer", URL: c.conn.URL(), + SDP: fmt.Sprintf("%+v", *c.config), Medias: c.medias, Receivers: c.receivers, Recv: int(recv), } return json.Marshal(info) } + +func (c *Client) trackByKind(kind string) *core.Receiver { + for _, receiver := range c.receivers { + if core.GetKind(receiver.Codec.Name) == kind { + return receiver + } + } + return nil +} diff --git a/scripts/README.md b/scripts/README.md index 789dab52..b48389cc 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -16,6 +16,32 @@ go mod why github.com/pion/rtcp go list -deps .\cmd\go2rtc_rtsp\ ``` +## Dependencies + +``` +- gopkg.in/yaml.v3 + - github.com/kr/pretty +- github.com/AlexxIT/go2rtc/pkg/mdns + - github.com/miekg/dns +- github.com/AlexxIT/go2rtc/pkg/pcm + - github.com/sigurn/crc16 + - github.com/sigurn/crc8 +- github.com/pion/ice/v2 + - github.com/google/uuid +- github.com/rs/zerolog + - github.com/mattn/go-colorable + - github.com/mattn/go-isatty +- ??? + - github.com/tadglines/go-pkgs + - github.com/davecgh/go-spew + - github.com/pmezard/go-difflib + - golang.org/x/crypto + - golang.org/x/mod + - golang.org/x/net + - golang.org/x/sys + - golang.org/x/tools +``` + ## Virus - https://go.dev/doc/faq#virus