Total rework HAP pkg and HomeKit source

This commit is contained in:
Alexey Khit
2023-07-23 22:22:36 +03:00
parent d73e9f6bcf
commit 6d82b1ce89
31 changed files with 2074 additions and 2015 deletions

15
go.mod
View File

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

24
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package camera
const TypeStreamingStatus = "120"
type StreamingStatus struct {
Status byte `tlv8:"1"`
}
const (
StreamingStatusAvailable = 0
StreamingStatusBusy = 1
StreamingStatusUnavailable = 2
)

View File

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

View File

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

View File

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

View File

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

328
pkg/hap/client.go Normal file
View File

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

60
pkg/hap/client_http.go Normal file
View File

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

376
pkg/hap/client_pairing.go Normal file
View File

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

View File

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

View File

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

View File

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

68
pkg/hap/event_reader.go Normal file
View File

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

View File

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

17
pkg/hap/hkdf/hkdf.go Normal file
View File

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

View File

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

View File

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

View File

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

156
pkg/hap/secure/secure.go Normal file
View File

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

View File

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

333
pkg/hap/tlv8/tlv8.go Normal file
View File

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

38
pkg/hap/tlv8/tlv8_test.go Normal file
View File

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

View File

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

View File

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