Merge branch 'feat/credential-login' into dev
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user