diff --git a/backend/isrc_finder.go b/backend/isrc_finder.go index e032526..8ca3394 100644 --- a/backend/isrc_finder.go +++ b/backend/isrc_finder.go @@ -1,9 +1,6 @@ package backend import ( - "crypto/hmac" - "crypto/sha1" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -20,16 +17,10 @@ import ( ) const ( - spotifyServerTimeURL = "https://open.spotify.com/api/server-time" - spotifySessionTokenURL = "https://open.spotify.com/api/token" - spotifyTOTPSecretsURL = "https://git.gay/thereallo/totp-secrets/raw/branch/main/secrets/secretDict.json" - spotifyGIDMetadataURL = "https://spclient.wg.spotify.com/metadata/4/%s/%s?market=from_token" - spotifyTOTPPeriod = 30 - spotifyTOTPDigits = 6 - spotifyBase62Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - spotifyTokenCacheFile = ".isrc-finder-token.json" - spotifySecretsCacheFile = "spotify-secret-dict-cache.json" - spotifySecretsCacheTTL = 24 * time.Hour + spotifySessionTokenURL = "https://open.spotify.com/api/token" + spotifyGIDMetadataURL = "https://spclient.wg.spotify.com/metadata/4/%s/%s?market=from_token" + spotifyBase62Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + spotifyTokenCacheFile = ".isrc-finder-token.json" ) var spotifyAnonymousTokenMu sync.Mutex @@ -39,15 +30,6 @@ type spotifyAnonymousToken struct { AccessTokenExpirationTimestampMs int64 `json:"accessTokenExpirationTimestampMs"` } -type spotifyServerTimeResponse struct { - ServerTime int64 `json:"serverTime"` -} - -type spotifySecretsCache struct { - FetchedAtUnix int64 `json:"fetched_at_unix"` - Secrets map[string][]int `json:"secrets"` -} - type spotifyTrackRawData struct { Album struct { GID string `json:"gid"` @@ -361,50 +343,6 @@ func saveSpotifyCachedToken(token *spotifyAnonymousToken) error { return nil } -func loadSpotifyCachedSecrets() (*spotifySecretsCache, error) { - cachePath, err := spotifySecretsCachePath() - if err != nil { - return nil, err - } - - body, err := os.ReadFile(cachePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, nil - } - return nil, fmt.Errorf("failed to read secrets cache: %w", err) - } - - var cache spotifySecretsCache - if err := json.Unmarshal(body, &cache); err != nil { - return nil, fmt.Errorf("failed to parse secrets cache: %w", err) - } - - return &cache, nil -} - -func saveSpotifyCachedSecrets(cache *spotifySecretsCache) error { - cachePath, err := spotifySecretsCachePath() - if err != nil { - return err - } - - if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil { - return fmt.Errorf("failed to create secrets cache directory: %w", err) - } - - body, err := json.MarshalIndent(cache, "", " ") - if err != nil { - return err - } - - if err := os.WriteFile(cachePath, body, 0o644); err != nil { - return fmt.Errorf("failed to write secrets cache: %w", err) - } - - return nil -} - func spotifyTokenCachePath() (string, error) { appDir, err := EnsureAppDir() if err != nil { @@ -414,15 +352,6 @@ func spotifyTokenCachePath() (string, error) { return filepath.Join(appDir, spotifyTokenCacheFile), nil } -func spotifySecretsCachePath() (string, error) { - appDir, err := EnsureAppDir() - if err != nil { - return "", err - } - - return filepath.Join(appDir, spotifySecretsCacheFile), nil -} - func spotifyTokenIsValid(token *spotifyAnonymousToken) bool { if token == nil || token.AccessToken == "" || token.AccessTokenExpirationTimestampMs == 0 { return false @@ -431,47 +360,6 @@ func spotifyTokenIsValid(token *spotifyAnonymousToken) bool { return time.Now().UnixMilli() < token.AccessTokenExpirationTimestampMs-30_000 } -func spotifySecretsCacheIsValid(cache *spotifySecretsCache) bool { - if cache == nil || cache.FetchedAtUnix == 0 || len(cache.Secrets) == 0 { - return false - } - - return time.Since(time.Unix(cache.FetchedAtUnix, 0)) < spotifySecretsCacheTTL -} - -func deriveSpotifyTOTPSecret(ciphertext []int) []byte { - var builder strings.Builder - - for index, value := range ciphertext { - builder.WriteString(strconv.Itoa(value ^ ((index % 33) + 9))) - } - - return []byte(builder.String()) -} - -func generateSpotifyTOTP(secret []byte, timestampMs int64) string { - counter := timestampMs / 1000 / spotifyTOTPPeriod - counterBytes := make([]byte, 8) - binary.BigEndian.PutUint64(counterBytes, uint64(counter)) - - mac := hmac.New(sha1.New, secret) - mac.Write(counterBytes) - digest := mac.Sum(nil) - - offset := digest[len(digest)-1] & 0x0f - binaryCode := (int(digest[offset])&0x7f)<<24 | - (int(digest[offset+1])&0xff)<<16 | - (int(digest[offset+2])&0xff)<<8 | - (int(digest[offset+3]) & 0xff) - - modulo := 1 - for i := 0; i < spotifyTOTPDigits; i++ { - modulo *= 10 - } - - return fmt.Sprintf("%0*d", spotifyTOTPDigits, binaryCode%modulo) -} - func requestSpotifyAnonymousAccessToken(client *http.Client) (string, error) { spotifyAnonymousTokenMu.Lock() defer spotifyAnonymousTokenMu.Unlock() @@ -485,52 +373,17 @@ func requestSpotifyAnonymousAccessToken(client *http.Client) (string, error) { return cachedToken.AccessToken, nil } - var serverTime spotifyServerTimeResponse - if err := requestSpotifyJSON(client, spotifyServerTimeURL, nil, &serverTime); err != nil { - return "", err - } - - var secrets map[string][]int - cachedSecrets, err := loadSpotifyCachedSecrets() + generatedTOTP, version, err := generateSpotifyTOTP(time.Now()) if err != nil { - fmt.Printf("Warning: failed to read Spotify secrets cache: %v\n", err) + return "", fmt.Errorf("failed to generate Spotify TOTP: %w", err) } - if spotifySecretsCacheIsValid(cachedSecrets) { - secrets = cachedSecrets.Secrets - } else { - if err := requestSpotifyJSON(client, spotifyTOTPSecretsURL, nil, &secrets); err != nil { - if cachedSecrets != nil && len(cachedSecrets.Secrets) > 0 { - fmt.Printf("Warning: failed to refresh Spotify secrets cache, using stale cache: %v\n", err) - secrets = cachedSecrets.Secrets - } else { - return "", err - } - } else { - cache := &spotifySecretsCache{ - FetchedAtUnix: time.Now().Unix(), - Secrets: secrets, - } - if err := saveSpotifyCachedSecrets(cache); err != nil { - fmt.Printf("Warning: failed to write Spotify secrets cache: %v\n", err) - } - } - } - - version, err := latestSpotifySecretVersion(secrets) - if err != nil { - return "", err - } - - secret := deriveSpotifyTOTPSecret(secrets[version]) - generatedTOTP := generateSpotifyTOTP(secret, serverTime.ServerTime*1000) - query := url.Values{ "reason": {"init"}, "productType": {"web-player"}, "totp": {generatedTOTP}, "totpServer": {generatedTOTP}, - "totpVer": {version}, + "totpVer": {strconv.Itoa(version)}, } var token spotifyAnonymousToken @@ -545,30 +398,6 @@ func requestSpotifyAnonymousAccessToken(client *http.Client) (string, error) { return token.AccessToken, nil } -func latestSpotifySecretVersion(secrets map[string][]int) (string, error) { - var ( - bestVersion string - bestNumber int - ) - - for version := range secrets { - number, err := strconv.Atoi(version) - if err != nil { - return "", fmt.Errorf("invalid secret version %q: %w", version, err) - } - if bestVersion == "" || number > bestNumber { - bestVersion = version - bestNumber = number - } - } - - if bestVersion == "" { - return "", errors.New("no TOTP secret versions available") - } - - return bestVersion, nil -} - func extractSpotifyTrackID(value string) (string, error) { value = strings.TrimSpace(value) if value == "" { diff --git a/backend/spotfetch.go b/backend/spotfetch.go index 4ca171c..f168005 100644 --- a/backend/spotfetch.go +++ b/backend/spotfetch.go @@ -15,9 +15,6 @@ import ( "time" "sort" - - "github.com/pquerna/otp" - "github.com/pquerna/otp/totp" ) var SpotifyError = errors.New("spotify error") @@ -40,21 +37,7 @@ func NewSpotifyClient() *SpotifyClient { } func (c *SpotifyClient) generateTOTP() (string, int, error) { - - secret := "GM3TMMJTGYZTQNZVGM4DINJZHA4TGOBYGMZTCMRTGEYDSMJRHE4TEOBUG4YTCMRUGQ4DQOJUGQYTAMRRGA2TCMJSHE3TCMBY" - version := 61 - - key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/secret?secret=%s", secret)) - if err != nil { - return "", 0, err - } - - totpCode, err := totp.GenerateCode(key.Secret(), time.Now()) - if err != nil { - return "", 0, err - } - - return totpCode, version, nil + return generateSpotifyTOTP(time.Now()) } func (c *SpotifyClient) getAccessToken() error { diff --git a/backend/spotify_totp.go b/backend/spotify_totp.go new file mode 100644 index 0000000..3f5faa5 --- /dev/null +++ b/backend/spotify_totp.go @@ -0,0 +1,28 @@ +package backend + +import ( + "fmt" + "time" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +const ( + spotifyTOTPSecret = "GM3TMMJTGYZTQNZVGM4DINJZHA4TGOBYGMZTCMRTGEYDSMJRHE4TEOBUG4YTCMRUGQ4DQOJUGQYTAMRRGA2TCMJSHE3TCMBY" + spotifyTOTPVersion = 61 +) + +func generateSpotifyTOTP(now time.Time) (string, int, error) { + key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/secret?secret=%s", spotifyTOTPSecret)) + if err != nil { + return "", 0, err + } + + code, err := totp.GenerateCode(key.Secret(), now) + if err != nil { + return "", 0, err + } + + return code, spotifyTOTPVersion, nil +}