From 42e7a0353444ceec2c36fc8c5037c7f2f4eb5b02 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Nov 2025 20:55:17 +0300 Subject: [PATCH] Add HomeKit QR code to WebUI #1138 by @mnakada --- internal/homekit/homekit.go | 4 +++- internal/homekit/server.go | 30 ++++++++++++++++++++++-------- pkg/hap/helpers.go | 8 ++++++++ pkg/hap/server.go | 8 -------- pkg/hap/setup/setup.go | 32 ++++++++++++++++++++++++++++++++ pkg/hap/setup/setup_test.go | 18 ++++++++++++++++++ www/links.html | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 pkg/hap/setup/setup.go create mode 100644 pkg/hap/setup/setup_test.go diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 98988806..59b84b3b 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -65,10 +65,12 @@ func Init() { deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address name := calcName(conf.Name, deviceID) + setupID := calcSetupID(id) srv := &server{ stream: id, pairings: conf.Pairings, + setupID: setupID, } srv.hap = &hap.Server{ @@ -90,7 +92,7 @@ func Init() { hap.TXTStateNumber: "1", hap.TXTStatusFlags: hap.StatusNotPaired, hap.TXTCategory: calcCategoryID(conf.CategoryID), - hap.TXTSetupHash: srv.hap.SetupHash(), + hap.TXTSetupHash: hap.SetupHash(setupID, deviceID), }, } entries = append(entries, srv.mdns) diff --git a/internal/homekit/server.go b/internal/homekit/server.go index 25082e4e..86cfbc15 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -40,20 +40,29 @@ type server struct { accessory *hap.Accessory // HAP accessory consumer *homekit.Consumer proxyURL string + setupID string stream string // stream name from YAML } func (s *server) MarshalJSON() ([]byte, error) { v := struct { - Name string `json:"name"` - DeviceID string `json:"device_id"` - Paired int `json:"paired"` - Conns []any `json:"connections"` + Name string `json:"name"` + DeviceID string `json:"device_id"` + Paired int `json:"paired,omitempty"` + CategoryID string `json:"category_id,omitempty"` + SetupCode string `json:"setup_code,omitempty"` + SetupID string `json:"setup_id,omitempty"` + Conns []any `json:"connections,omitempty"` }{ - Name: s.mdns.Name, - DeviceID: s.mdns.Info[hap.TXTDeviceID], - Paired: len(s.pairings), - Conns: s.conns, + Name: s.mdns.Name, + DeviceID: s.mdns.Info[hap.TXTDeviceID], + CategoryID: s.mdns.Info[hap.TXTCategory], + Paired: len(s.pairings), + Conns: s.conns, + } + if v.Paired == 0 { + v.SetupCode = s.hap.Pin + v.SetupID = s.setupID } return json.Marshal(v) } @@ -377,6 +386,11 @@ func calcDevicePrivate(private, seed string) []byte { return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize]) } +func calcSetupID(seed string) string { + b := sha512.Sum512([]byte(seed)) + return fmt.Sprintf("%02X%02X", b[44], b[46]) +} + func calcCategoryID(categoryID string) string { switch categoryID { case "bridge": diff --git a/pkg/hap/helpers.go b/pkg/hap/helpers.go index 3900f935..3c3b287c 100644 --- a/pkg/hap/helpers.go +++ b/pkg/hap/helpers.go @@ -3,6 +3,8 @@ package hap import ( "crypto/ed25519" "crypto/rand" + "crypto/sha512" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -99,6 +101,12 @@ func GenerateUUID() string { return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] } +func SetupHash(setupID, deviceID string) string { + // should be setup_id (random 4 alphanum) + device_id (mac address) + b := sha512.Sum512([]byte(setupID + deviceID)) + return base64.StdEncoding.EncodeToString(b[:4]) +} + func Append(items ...any) (b []byte) { for _, item := range items { switch v := item.(type) { diff --git a/pkg/hap/server.go b/pkg/hap/server.go index f962a440..a992528c 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -3,7 +3,6 @@ package hap import ( "bufio" "crypto/sha512" - "encoding/base64" "errors" "fmt" "net/http" @@ -36,13 +35,6 @@ func (s *Server) ServerPublic() []byte { // return StatusPaired //} -func (s *Server) SetupHash() string { - // should be setup_id (random 4 alphanum) + device_id (mac address) - // but device_id is random, so OK - b := sha512.Sum512([]byte(s.DeviceID)) - return base64.StdEncoding.EncodeToString(b[:4]) -} - func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) { // STEP 1. Request from iPhone var plainM1 struct { diff --git a/pkg/hap/setup/setup.go b/pkg/hap/setup/setup.go new file mode 100644 index 00000000..c5eeb51b --- /dev/null +++ b/pkg/hap/setup/setup.go @@ -0,0 +1,32 @@ +package setup + +import ( + "strconv" + "strings" +) + +const ( + FlagNFC = 1 + FlagIP = 2 + FlagBLE = 4 + FlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi +) + +func GenerateSetupURI(category, pin, setupID string) string { + c, _ := strconv.Atoi(category) + p, _ := strconv.Atoi(strings.ReplaceAll(pin, "-", "")) + payload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF) + return "X-HM://" + FormatInt36(payload, 9) + setupID +} + +// FormatInt36 equal to strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36))) +func FormatInt36(value int64, n int) string { + b := make([]byte, n) + for i := n - 1; 0 <= i; i-- { + b[i] = digits[value%36] + value /= 36 + } + return string(b) +} + +const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/pkg/hap/setup/setup_test.go b/pkg/hap/setup/setup_test.go new file mode 100644 index 00000000..01672218 --- /dev/null +++ b/pkg/hap/setup/setup_test.go @@ -0,0 +1,18 @@ +package setup + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFormatAlphaNum(t *testing.T) { + value := int64(999) + n := 5 + s1 := strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36))) + s2 := FormatInt36(value, n) + require.Equal(t, s1, s2) +} diff --git a/www/links.html b/www/links.html index a54fcf8f..36cb7a7e 100644 --- a/www/links.html +++ b/www/links.html @@ -71,6 +71,42 @@ }); + + +

Play audio