From c6fbc45be552be50893ea4a15843589fe7300fd0 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Tue, 28 Apr 2026 17:36:25 +0200 Subject: [PATCH] lint --- .../bose-soundtouch-community-tools.md | 1 - pkg/service/datastore/datastore.go | 12 ++++++--- pkg/service/handlers/handlers_marge.go | 6 ++--- pkg/service/handlers/handlers_proxy.go | 1 + pkg/service/handlers/server.go | 1 + pkg/service/spotify/zeroconf.go | 27 ++++++++++++++++--- pkg/service/spotify/zeroconf_test.go | 7 +++-- 7 files changed, 40 insertions(+), 15 deletions(-) diff --git a/docs/analysis/bose-soundtouch-community-tools.md b/docs/analysis/bose-soundtouch-community-tools.md index a66a33b..2de2b6f 100644 --- a/docs/analysis/bose-soundtouch-community-tools.md +++ b/docs/analysis/bose-soundtouch-community-tools.md @@ -71,7 +71,6 @@ The only native installable phone app in the ecosystem. Pairs with the Überbös ### 4. SoundTouch Hybrid 2026 **[github.com/TJGigs/Bose-SoundTouch-Hybrid-2026](https://github.com/TJGigs/Bose-SoundTouch-Hybrid-2026)** -(V3 variant: [github.com/TJGigs/Bose-SoundTouch-Hybrid-2026-V3](https://github.com/TJGigs/Bose-SoundTouch-Hybrid-2026-V3)) | | | |---|---| | Language | Node.js (JavaScript) | diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index 0f9c4e9..706afaf 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "math/rand" @@ -23,6 +24,9 @@ import ( "github.com/gesellix/bose-soundtouch/pkg/service/constants" ) +// ErrGroupNotFound is returned when no group is found for a given device. +var ErrGroupNotFound = errors.New("group not found") + func exists(path string) bool { _, err := os.Stat(path) return err == nil @@ -1933,7 +1937,7 @@ func (ds *DataStore) GetGroupForDevice(account, deviceID string) (*models.Group, entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { - return nil, nil + return nil, ErrGroupNotFound } return nil, err @@ -1961,7 +1965,7 @@ func (ds *DataStore) GetGroupForDevice(account, deviceID string) (*models.Group, } } - return nil, nil + return nil, ErrGroupNotFound } // AddGroup saves a new group to disk and returns its generated ID. @@ -2002,8 +2006,8 @@ func (ds *DataStore) ModifyGroup(account, groupID, newName string) (*models.Grou } var g models.Group - if err := xml.Unmarshal(data, &g); err != nil { - return nil, err + if xmlErr := xml.Unmarshal(data, &g); xmlErr != nil { + return nil, xmlErr } g.Name = newName diff --git a/pkg/service/handlers/handlers_marge.go b/pkg/service/handlers/handlers_marge.go index 12e3587..e89f19f 100644 --- a/pkg/service/handlers/handlers_marge.go +++ b/pkg/service/handlers/handlers_marge.go @@ -687,7 +687,7 @@ func (s *Server) HandleMargeDeviceGroup(w http.ResponseWriter, r *http.Request) w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml") group, err := s.ds.GetGroupForDevice(account, device) - if err != nil || group == nil { + if err != nil { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(constants.XMLHeader + ``)) @@ -732,7 +732,7 @@ func (s *Server) HandleMargeAddGroup(w http.ResponseWriter, r *http.Request) { } var group models.Group - if err := xml.Unmarshal(body, &group); err != nil { + if xmlErr := xml.Unmarshal(body, &group); xmlErr != nil { http.Error(w, "Invalid XML", http.StatusBadRequest) return } @@ -773,7 +773,7 @@ func (s *Server) HandleMargeModifyGroup(w http.ResponseWriter, r *http.Request) } var req models.Group - if err := xml.Unmarshal(body, &req); err != nil { + if xmlErr := xml.Unmarshal(body, &req); xmlErr != nil { http.Error(w, "Invalid XML", http.StatusBadRequest) return } diff --git a/pkg/service/handlers/handlers_proxy.go b/pkg/service/handlers/handlers_proxy.go index f74910f..2af5bc7 100644 --- a/pkg/service/handlers/handlers_proxy.go +++ b/pkg/service/handlers/handlers_proxy.go @@ -113,6 +113,7 @@ func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) { preview := body truncated := "" + if len(preview) > 512 { preview = preview[:512] truncated = "…" diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index 305b4cb..d5c4fa2 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -470,6 +470,7 @@ func (s *Server) pushSpotifyTokenToDevice(deviceIP, username, accessToken string } else { zcURL = fmt.Sprintf("http://%s:8200/zc", deviceIP) } + return spotify.PushSpotifyCredentials(zcURL, username, accessToken) } diff --git a/pkg/service/spotify/zeroconf.go b/pkg/service/spotify/zeroconf.go index 0a05db6..4c8624d 100644 --- a/pkg/service/spotify/zeroconf.go +++ b/pkg/service/spotify/zeroconf.go @@ -51,9 +51,11 @@ func generateDHKeyPair() (privateKey *big.Int, publicKeyBytes []byte, err error) if _, err = rand.Read(privBytes); err != nil { return } + privateKey = new(big.Int).SetBytes(privBytes) pub := new(big.Int).Exp(dhGenerator, privateKey, dhPrime) publicKeyBytes = padBigInt(pub, dhKeySize) + return } @@ -61,6 +63,7 @@ func generateDHKeyPair() (privateKey *big.Int, publicKeyBytes []byte, err error) func computeSharedSecret(privateKey *big.Int, remotePublicKeyBytes []byte) []byte { remote := new(big.Int).SetBytes(remotePublicKeyBytes) shared := new(big.Int).Exp(remote, privateKey, dhPrime) + return padBigInt(shared, dhKeySize) } @@ -76,6 +79,7 @@ func deriveKeys(sharedSecret []byte) (encKey, macKey []byte) { hMac := hmac.New(sha1.New, baseKey) hMac.Write([]byte("checksum")) macKey = hMac.Sum(nil) + return } @@ -125,6 +129,7 @@ func encryptBlob(encKey, macKey, plaintext []byte) ([]byte, error) { out = append(out, iv...) out = append(out, ciphertext...) out = append(out, mac.Sum(nil)...) + return out, nil } @@ -141,6 +146,7 @@ func decryptBlob(encKey, macKey, blob []byte) ([]byte, error) { mac := hmac.New(sha1.New, macKey) mac.Write(ciphertext) + if !hmac.Equal(mac.Sum(nil), gotMAC) { return nil, fmt.Errorf("blob HMAC verification failed") } @@ -152,16 +158,19 @@ func decryptBlob(encKey, macKey, blob []byte) ([]byte, error) { plaintext := make([]byte, len(ciphertext)) cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext) + return plaintext, nil } // ZeroConfGetInfo fetches the speaker's DH public key via GET ?action=getInfo. func ZeroConfGetInfo(zcBaseURL string) ([]byte, error) { client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(zcBaseURL + "?action=getInfo") if err != nil { return nil, fmt.Errorf("getInfo: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -169,9 +178,10 @@ func ZeroConfGetInfo(zcBaseURL string) ([]byte, error) { } var info zcGetInfoResponse - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - return nil, fmt.Errorf("getInfo: decode: %w", err) + if decodeErr := json.NewDecoder(resp.Body).Decode(&info); decodeErr != nil { + return nil, fmt.Errorf("getInfo: decode: %w", decodeErr) } + if info.PublicKey == "" { return nil, fmt.Errorf("getInfo: empty publicKey") } @@ -184,6 +194,7 @@ func ZeroConfGetInfo(zcBaseURL string) ([]byte, error) { return nil, fmt.Errorf("getInfo: invalid base64 publicKey: %w", err) } } + return pubKey, nil } @@ -208,6 +219,7 @@ func PushSpotifyCredentials(zcBaseURL, username, accessToken string) error { encKey, macKey := deriveKeys(sharedSecret) plaintext := buildCredentialsBlob(username, accessToken) + encryptedBlob, err := encryptBlob(encKey, macKey, plaintext) if err != nil { return fmt.Errorf("pushSpotifyCredentials: encrypt: %w", err) @@ -219,16 +231,19 @@ func PushSpotifyCredentials(zcBaseURL, username, accessToken string) error { data.Set("clientKey", base64.StdEncoding.EncodeToString(ourPublicKeyBytes)) client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.PostForm(zcBaseURL+"?action=addUser", data) if err != nil { return fmt.Errorf("pushSpotifyCredentials: addUser: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("pushSpotifyCredentials: addUser status %d: %s", resp.StatusCode, body) } + return nil } @@ -243,16 +258,19 @@ func pushSimplifiedToken(zcBaseURL, username, accessToken string) error { data.Set("tokenType", "accesstoken") client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.PostForm(zcBaseURL+"?action=addUser", data) if err != nil { return fmt.Errorf("pushSimplifiedToken: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("pushSimplifiedToken: status %d: %s", resp.StatusCode, body) } + return nil } @@ -261,8 +279,10 @@ func padBigInt(n *big.Int, size int) []byte { if len(b) >= size { return b } + out := make([]byte, size) copy(out[size-len(b):], b) + return out } @@ -271,5 +291,6 @@ func writeVarint(buf *bytes.Buffer, v uint64) { buf.WriteByte(byte(v) | 0x80) v >>= 7 } + buf.WriteByte(byte(v)) -} \ No newline at end of file +} diff --git a/pkg/service/spotify/zeroconf_test.go b/pkg/service/spotify/zeroconf_test.go index 3e8cf03..7fb6f9f 100644 --- a/pkg/service/spotify/zeroconf_test.go +++ b/pkg/service/spotify/zeroconf_test.go @@ -152,9 +152,9 @@ func TestPushSpotifyCredentials_FullRoundTrip(t *testing.T) { case "getInfo": w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": 101, + "status": 101, "statusString": "OK", - "publicKey": base64.StdEncoding.EncodeToString(speakerPublicBytes), + "publicKey": base64.StdEncoding.EncodeToString(speakerPublicBytes), }) case "addUser": @@ -311,7 +311,6 @@ func parseCredentialsBlob(data []byte) (*parsedCredentials, error) { return &r, nil } - func readProtoVarint(data []byte) (uint64, int) { var val uint64 for i, b := range data { @@ -321,4 +320,4 @@ func readProtoVarint(data []byte) (uint64, int) { } } return 0, len(data) -} \ No newline at end of file +}