.unified totp

This commit is contained in:
afkarxyz
2026-04-13 22:43:35 +07:00
parent 5a3f819cef
commit e23fa2a48e
3 changed files with 36 additions and 196 deletions
+3 -174
View File
@@ -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
)
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 == "" {
+1 -18
View File
@@ -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 {
+28
View File
@@ -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
}