diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..4c7cd1e --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + + "github.com/mathismqn/godeez/internal/auth" + "github.com/spf13/cobra" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Log in to Deezer with your email and password", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := auth.CheckGatewayEnv(); err != nil { + return err + } + + email, password, err := auth.PromptCredentials() + if err != nil { + return err + } + + _, username, err := auth.Login(cmd.Context(), email, password) + if err != nil { + return err + } + + fmt.Printf("Successfully logged in as %s.\n", username) + + return nil + }, +} + +func init() { + RootCmd.AddCommand(loginCmd) +} diff --git a/cmd/logout.go b/cmd/logout.go new file mode 100644 index 0000000..3f6e537 --- /dev/null +++ b/cmd/logout.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + + "github.com/mathismqn/godeez/internal/auth" + "github.com/spf13/cobra" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Remove stored Deezer credentials", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := auth.Clear(); err != nil { + return err + } + + fmt.Println("Successfully logged out.") + + return nil + }, +} + +func init() { + RootCmd.AddCommand(logoutCmd) +} diff --git a/go.mod b/go.mod index 30f1933..59c25e1 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,21 @@ module github.com/mathismqn/godeez go 1.25.0 -require github.com/spf13/cobra v1.10.2 +require ( + github.com/spf13/cobra v1.10.2 + github.com/zalando/go-keyring v0.2.8 + golang.org/x/term v0.45.0 +) require ( github.com/andybalholm/cascadia v1.3.4 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect github.com/fatih/color v1.19.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 0e6b2c8..fcd2e73 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDay github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= 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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -19,6 +21,8 @@ github.com/go-flac/flacvorbis/v2 v2.0.2 h1:xCL3OhxrxWkHrbWUBvGNe+6FQ03yLmBbz0v5z github.com/go-flac/flacvorbis/v2 v2.0.2/go.mod h1:SwTB5gs13VaM/N7rstwPoUsPibiMKklgwybYP9dYo2g= github.com/go-flac/go-flac/v2 v2.0.4 h1:atf/kFa8U9idtkA//NO22XGr+MzQLeXZecnmP9sYBf0= github.com/go-flac/go-flac/v2 v2.0.4/go.mod h1:sYOlTKxutMW0RDYF+KlD6Zn+VOCZlIFQG/r/usPveCs= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -33,9 +37,13 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..88ac6e2 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "fmt" +) + +func Resolve(ctx context.Context, validate func(ctx context.Context, arl string) error) (string, error) { + creds, err := Load() + if err != nil { + return "", err + } + + if creds != nil && creds.ARL != "" { + if verr := validate(ctx, creds.ARL); verr == nil { + return creds.ARL, nil + } + } + + if creds != nil && creds.Email != "" && creds.Password != "" { + arl, _, err := Login(ctx, creds.Email, creds.Password) + if err != nil { + return "", fmt.Errorf("stored session expired and could not be renewed automatically: %w", err) + } + + return arl, nil + } + + return "", fmt.Errorf("run 'godeez login' or export DEEZER_ARL environment variable") +} + +func Login(ctx context.Context, email, password string) (string, string, error) { + client, err := newMobileClient() + if err != nil { + return "", "", err + } + + creds, username, err := client.Login(ctx, email, password) + if err != nil { + return "", "", err + } + + if err := Save(creds); err != nil { + return "", "", err + } + + return creds.ARL, username, nil +} diff --git a/internal/auth/mobile.go b/internal/auth/mobile.go new file mode 100644 index 0000000..0e86f81 --- /dev/null +++ b/internal/auth/mobile.go @@ -0,0 +1,266 @@ +package auth + +import ( + "bytes" + "context" + "crypto/aes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/rand/v2" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/mathismqn/godeez/internal/crypto" +) + +const ( + gatewayBaseURL = "https://api.deezer.com/1.0/gateway.php" + gatewayUserAgent = "Deezer/6.1.22.49 (Android; 9; Tablet; us) innotek GmbH VirtualBox" + nonceAlphabet = "012345689abdef" +) + +const ( + deviceOS = "Android" + deviceName = "VirtualBox" + deviceType = "tablet" + deviceModel = "VirtualBox" + devicePlatform = "innotek GmbH_x86_64_9" + deviceSerial = "" +) + +type mobileClient struct { + httpClient *http.Client + apiKey string + gwKey []byte + sid string +} + +func CheckGatewayEnv() error { + _, _, err := gatewayEnv() + return err +} + +func gatewayEnv() (string, string, error) { + apiKey := os.Getenv("DEEZER_MOBILE_API_KEY") + gwKey := os.Getenv("DEEZER_MOBILE_GW_KEY") + if apiKey == "" || gwKey == "" { + return "", "", fmt.Errorf("DEEZER_MOBILE_API_KEY and DEEZER_MOBILE_GW_KEY must be set to use email/password login") + } + + if len(gwKey) != aes.BlockSize { + return "", "", fmt.Errorf("DEEZER_MOBILE_GW_KEY must be exactly %d bytes long", aes.BlockSize) + } + + return apiKey, gwKey, nil +} + +func newMobileClient() (*mobileClient, error) { + apiKey, gwKey, err := gatewayEnv() + if err != nil { + return nil, err + } + + return &mobileClient{ + httpClient: &http.Client{Timeout: 20 * time.Second}, + apiKey: apiKey, + gwKey: []byte(gwKey), + }, nil +} + +func (m *mobileClient) Login(ctx context.Context, email, password string) (*Credentials, string, error) { + token, tokenKey, userKey, err := m.authenticate(ctx) + if err != nil { + return nil, "", err + } + + if err := m.checkToken(ctx, token, tokenKey); err != nil { + return nil, "", err + } + + arl, username, err := m.userAuth(ctx, email, password, userKey) + if err != nil { + return nil, "", err + } + + return &Credentials{Email: email, Password: password, ARL: arl}, username, nil +} + +func (m *mobileClient) authenticate(ctx context.Context) (string, string, string, error) { + body, err := m.gatewayRequest(ctx, "mobile_auth", "GET", "uniq_id", genUniqID(), nil) + if err != nil { + return "", "", "", err + } + + var res struct { + Results struct { + Token string `json:"TOKEN"` + } `json:"results"` + } + if err := json.Unmarshal(body, &res); err != nil { + return "", "", "", err + } + + if strings.Contains(string(body), "Undefined or invalid API key") { + return "", "", "", fmt.Errorf("DEEZER_MOBILE_API_KEY is invalid") + } + if strings.Contains(string(body), "GATEWAY_ERROR") || res.Results.Token == "" { + return "", "", "", fmt.Errorf("unexpected response from gateway") + } + + encrypted, err := hex.DecodeString(res.Results.Token) + if err != nil { + return "", "", "", err + } + + decrypted, err := crypto.DecryptECB(m.gwKey, encrypted) + if err != nil { + return "", "", "", err + } + + token := string(decrypted[0:64]) + tokenKey := string(decrypted[64:80]) + userKey := string(decrypted[80:96]) + + return token, tokenKey, userKey, nil +} + +func (m *mobileClient) checkToken(ctx context.Context, token, tokenKey string) error { + encrypted, err := crypto.EncryptECB([]byte(tokenKey), []byte(token)) + if err != nil { + return err + } + authToken := hex.EncodeToString(encrypted) + + body, err := m.gatewayRequest(ctx, "api_checkToken", "GET", "auth_token", authToken, nil) + if err != nil { + return err + } + + var res struct { + Results string `json:"results"` + } + if err := json.Unmarshal(body, &res); err != nil { + return err + } + if res.Results == "" { + return fmt.Errorf("unexpected response from gateway") + } + m.sid = res.Results + + return nil +} + +func (m *mobileClient) userAuth(ctx context.Context, email, password, userKey string) (string, string, error) { + encryptedPassword, err := crypto.EncryptECB([]byte(userKey), crypto.ZeroPad([]byte(password))) + if err != nil { + return "", "", err + } + + payload := map[string]string{ + "mail": email, + "password": hex.EncodeToString(encryptedPassword), + "device_serial": deviceSerial, + "platform": devicePlatform, + "custo_version_id": "", + "custo_partner": "", + "model": deviceModel, + "device_name": deviceName, + "device_os": deviceOS, + "device_type": deviceType, + "google_play_services_availability": "1", + "consent_string": "", + } + + jsonBody, err := json.Marshal(payload) + if err != nil { + return "", "", err + } + + body, err := m.gatewayRequest(ctx, "mobile_userAuth", "POST", "", "", jsonBody) + if err != nil { + return "", "", err + } + + if strings.Contains(string(body), "USER_AUTH_ERROR") { + return "", "", fmt.Errorf("invalid email or password") + } + + var res struct { + Results struct { + ARL string `json:"ARL"` + UserID int `json:"USER_ID"` + BlogName string `json:"BLOG_NAME"` + } `json:"results"` + } + if err := json.Unmarshal(body, &res); err != nil { + return "", "", err + } + + if res.Results.ARL == "" || res.Results.UserID == 0 { + return "", "", fmt.Errorf("unexpected response from gateway") + } + + return res.Results.ARL, res.Results.BlogName, nil +} + +func (m *mobileClient) gatewayRequest(ctx context.Context, method, httpMethod, paramKey, paramValue string, jsonBody []byte) ([]byte, error) { + u, err := url.Parse(gatewayBaseURL) + if err != nil { + return nil, err + } + + q := u.Query() + q.Set("method", method) + q.Set("api_key", m.apiKey) + q.Set("output", "3") + if httpMethod == "POST" { + q.Set("input", "3") + } + if m.sid != "" { + q.Set("sid", m.sid) + } + if paramKey != "" { + q.Set(paramKey, paramValue) + } + u.RawQuery = q.Encode() + + var reqBody io.Reader + if jsonBody != nil { + reqBody = bytes.NewReader(jsonBody) + } + + req, err := http.NewRequestWithContext(ctx, httpMethod, u.String(), reqBody) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", gatewayUserAgent) + if jsonBody != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +func genUniqID() string { + b := make([]byte, 32) + for i := range b { + b[i] = nonceAlphabet[rand.IntN(len(nonceAlphabet))] + } + + return string(b) +} diff --git a/internal/auth/prompt.go b/internal/auth/prompt.go new file mode 100644 index 0000000..72e4dc1 --- /dev/null +++ b/internal/auth/prompt.go @@ -0,0 +1,28 @@ +package auth + +import ( + "bufio" + "fmt" + "os" + "strings" + + "golang.org/x/term" +) + +func PromptCredentials() (string, string, error) { + fmt.Print("Email: ") + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return "", "", err + } + email := strings.TrimSpace(line) + + fmt.Print("Password: ") + passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return "", "", err + } + + return email, string(passwordBytes), nil +} diff --git a/internal/auth/storage.go b/internal/auth/storage.go new file mode 100644 index 0000000..627d7b2 --- /dev/null +++ b/internal/auth/storage.go @@ -0,0 +1,61 @@ +package auth + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/zalando/go-keyring" +) + +const ( + keyringService = "godeez" + keyringUser = "default" +) + +type Credentials struct { + Email string `json:"email"` + Password string `json:"password"` + ARL string `json:"arl,omitempty"` +} + +func Load() (*Credentials, error) { + secret, err := keyring.Get(keyringService, keyringUser) + if err != nil { + if errors.Is(err, keyring.ErrNotFound) { + return nil, nil + } + return nil, fmt.Errorf("system keyring is unavailable: %v", err) + } + + var creds Credentials + if err := json.Unmarshal([]byte(secret), &creds); err != nil { + return nil, err + } + + return &creds, nil +} + +func Save(creds *Credentials) error { + data, err := json.Marshal(creds) + if err != nil { + return err + } + + if err := keyring.Set(keyringService, keyringUser, string(data)); err != nil { + return fmt.Errorf("system keyring is unavailable: %v", err) + } + + return nil +} + +func Clear() error { + if err := keyring.Delete(keyringService, keyringUser); err != nil { + if errors.Is(err, keyring.ErrNotFound) { + return nil + } + return fmt.Errorf("system keyring is unavailable: %v", err) + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 9447943..f669c81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,16 +10,13 @@ import ( ) type Config struct { - ArlCookie string + ARLCookie string OutputDir string HomeDir string } func New() (*Config, error) { arl := os.Getenv("DEEZER_ARL") - if arl == "" { - return nil, fmt.Errorf("DEEZER_ARL is not set. Export it in your shell: export DEEZER_ARL='your_cookie'") - } homeDir, err := os.UserHomeDir() if err != nil { @@ -50,7 +47,7 @@ func New() (*Config, error) { } return &Config{ - ArlCookie: arl, + ARLCookie: arl, OutputDir: outputDir, HomeDir: homeDir, }, nil diff --git a/internal/crypto/blowfish.go b/internal/crypto/blowfish.go index 787dc1a..dc588d7 100644 --- a/internal/crypto/blowfish.go +++ b/internal/crypto/blowfish.go @@ -9,31 +9,31 @@ import ( ) var ( - iv = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} - secretKey = []byte("g4el58wc0zvf9na1") + blowfishIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} + blowfishSecretKey = []byte("g4el58wc0zvf9na1") ) -func GetKey(songID string) []byte { +func GetBlowfishKey(songID string) []byte { hash := md5.Sum([]byte(songID)) hashHex := hex.EncodeToString(hash[:]) - key := make([]byte, len(secretKey)) - copy(key, secretKey) - for i := 0; i < len(hash); i++ { + key := make([]byte, len(blowfishSecretKey)) + copy(key, blowfishSecretKey) + for i := range len(hash) { key[i] = key[i] ^ hashHex[i] ^ hashHex[i+16] } return key } -func Decrypt(data, key []byte) ([]byte, error) { +func DecryptBlowfish(data, key []byte) ([]byte, error) { block, err := blowfish.NewCipher(key) if err != nil { return nil, err } decrypted := make([]byte, len(data)) - cipher.NewCBCDecrypter(block, iv).CryptBlocks(decrypted, data) + cipher.NewCBCDecrypter(block, blowfishIV).CryptBlocks(decrypted, data) return decrypted, nil } diff --git a/internal/crypto/ecb.go b/internal/crypto/ecb.go new file mode 100644 index 0000000..14c994c --- /dev/null +++ b/internal/crypto/ecb.go @@ -0,0 +1,42 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" +) + +func ZeroPad(data []byte) []byte { + bs := aes.BlockSize + padded := make([]byte, len(data)+(bs-len(data)%bs)%bs) + copy(padded, data) + + return padded +} + +func EncryptECB(key, data []byte) ([]byte, error) { + return ecbTransform(key, data, (cipher.Block).Encrypt) +} + +func DecryptECB(key, data []byte) ([]byte, error) { + return ecbTransform(key, data, (cipher.Block).Decrypt) +} + +func ecbTransform(key, data []byte, op func(cipher.Block, []byte, []byte)) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + bs := block.BlockSize() + if len(data)%bs != 0 { + return nil, fmt.Errorf("data length %d is not a multiple of the AES block size", len(data)) + } + + out := make([]byte, len(data)) + for i := 0; i < len(data); i += bs { + op(block, out[i:i+bs], data[i:i+bs]) + } + + return out, nil +} diff --git a/internal/deezer/client.go b/internal/deezer/client.go index ff443d8..f3faab9 100644 --- a/internal/deezer/client.go +++ b/internal/deezer/client.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/mathismqn/godeez/internal/auth" "github.com/mathismqn/godeez/internal/config" ) @@ -18,7 +19,7 @@ type Client struct { } func NewClient(ctx context.Context, appConfig *config.Config) (*Client, error) { - session, err := Authenticate(ctx, appConfig.ArlCookie) + session, err := resolveSession(ctx, appConfig) if err != nil { return nil, fmt.Errorf("failed to authenticate: %w", err) } @@ -29,6 +30,37 @@ func NewClient(ctx context.Context, appConfig *config.Config) (*Client, error) { }, nil } +func resolveSession(ctx context.Context, appConfig *config.Config) (*Session, error) { + if appConfig.ARLCookie != "" { + return Authenticate(ctx, appConfig.ARLCookie) + } + + var session *Session + validate := func(ctx context.Context, arl string) error { + s, err := Authenticate(ctx, arl) + if err != nil { + return err + } + session = s + + return nil + } + + arl, err := auth.Resolve(ctx, validate) + if err != nil { + return nil, err + } + + if session == nil { + session, err = Authenticate(ctx, arl) + if err != nil { + return nil, err + } + } + + return session, nil +} + func (c *Client) FetchResource(ctx context.Context, resource Resource, id string) error { payload := map[string]interface{}{ "nb": 10000, diff --git a/internal/deezer/session.go b/internal/deezer/session.go index c4c1d60..adaee54 100644 --- a/internal/deezer/session.go +++ b/internal/deezer/session.go @@ -71,7 +71,7 @@ func Authenticate(ctx context.Context, arlCookie string) (*Session, error) { } if res.Results.User.ID == 0 { - return nil, fmt.Errorf("invalid arl cookie") + return nil, fmt.Errorf("invalid or expired ARL cookie") } opts := res.Results.User.Options diff --git a/internal/downloader/client.go b/internal/downloader/client.go index 4c7ba86..0c1d561 100644 --- a/internal/downloader/client.go +++ b/internal/downloader/client.go @@ -175,7 +175,7 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son fileName := song.GetFileName(c.resourceType, mediaFormat) outputPath := path.Join(outputDir, fileName) - key := crypto.GetKey(song.ID) + key := crypto.GetBlowfishKey(song.ID) if err := c.streamToFile(dlCtx, stream, outputPath, key); err != nil { fileutil.DeleteFile(outputPath) return downloadResult{err: fmt.Errorf("failed to stream to file: %w", err)} @@ -233,7 +233,7 @@ func (c *Client) streamToFile(ctx context.Context, stream io.ReadCloser, outputP } if chunk%3 == 0 && totalRead == chunkSize { - buffer, err = crypto.Decrypt(buffer, key) + buffer, err = crypto.DecryptBlowfish(buffer, key) if err != nil { return err }