From e06a21123f7c28507be9ae4ac3e336ecf4f7a4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 8 Sep 2025 23:30:27 +0900 Subject: [PATCH] feat: QQMusic Mac v10 mmkv support --- algo/qmc/key_mmkv.go | 45 ++---------- algo/qmc/key_mmkv_loader_darwin.go | 107 +++++++++++++++++++++++++++-- go.mod | 2 +- go.sum | 2 + 4 files changed, 110 insertions(+), 46 deletions(-) diff --git a/algo/qmc/key_mmkv.go b/algo/qmc/key_mmkv.go index fd025e3..1ed941f 100644 --- a/algo/qmc/key_mmkv.go +++ b/algo/qmc/key_mmkv.go @@ -3,7 +3,6 @@ package qmc import ( "fmt" "os" - "path/filepath" "git.um-react.app/um/cli/algo/common" "git.um-react.app/um/cli/internal/utils" @@ -33,9 +32,11 @@ func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKey defer mr.Close() cr, err := os.Open(mmkv_crc) - if err == nil { + if err != nil { // crc is optional - cr = nil + logger.Warn("LoadMMKV: Failed to open crc file, assuming no encryption", zap.Error(err)) + key = "" + } else { defer cr.Close() } @@ -68,41 +69,3 @@ func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKey return result, nil } - -// getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour) -func getRelativeMMKVDir(file string) (string, error) { - mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv") - if _, err := os.Stat(mmkvDir); err != nil { - return "", fmt.Errorf("stat default mmkv dir: %w", err) - } - - keyFile := filepath.Join(mmkvDir, "MMKVStreamEncryptId") - if _, err := os.Stat(keyFile); err != nil { - return "", fmt.Errorf("stat default mmkv file: %w", err) - } - - return mmkvDir, nil -} - -func getDefaultMMKVDir() (string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("get user home dir: %w", err) - } - - mmkvDir := filepath.Join( - homeDir, - "Library/Containers/com.tencent.QQMusicMac/Data", - "Library/Application Support/QQMusicMac/mmkv", - ) - if _, err := os.Stat(mmkvDir); err != nil { - return "", fmt.Errorf("stat default mmkv dir: %w", err) - } - - keyFile := filepath.Join(mmkvDir, "MMKVStreamEncryptId") - if _, err := os.Stat(keyFile); err != nil { - return "", fmt.Errorf("stat default mmkv file: %w", err) - } - - return mmkvDir, nil -} diff --git a/algo/qmc/key_mmkv_loader_darwin.go b/algo/qmc/key_mmkv_loader_darwin.go index 1d66c6a..bcef203 100644 --- a/algo/qmc/key_mmkv_loader_darwin.go +++ b/algo/qmc/key_mmkv_loader_darwin.go @@ -1,9 +1,13 @@ package qmc import ( + "crypto/md5" "fmt" "os" "path/filepath" + "regexp" + "strconv" + "strings" "git.um-react.app/um/cli/algo/common" "go.uber.org/zap" @@ -58,8 +62,103 @@ func loadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) { return nil, nil } -func loadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) { - // TODO: stub only - var _ = logger - return nil, nil +var _RE_UDID = regexp.MustCompile(`_\x10\(([0-9a-f]{40})_`) + +func extractUdid(data []byte) (string, error) { + match := _RE_UDID.FindSubmatch(data) + if len(match) != 2 { + return "", fmt.Errorf("extractUdid: could not find udid") + } + return string(match[1]), nil +} + +func caesar(text string, shift int) string { + var result strings.Builder + + for _, char := range []byte(text) { + var transformed byte + if 'A' <= char && char <= 'Z' { + transformed = (char-'A'+byte(shift))%26 + 'A' + } else if 'a' <= char && char <= 'z' { + transformed = (char-'a'+byte(shift))%26 + 'a' + } else if '0' <= char && char <= '9' { + transformed = (char-'0'+byte(shift))%10 + '0' + } else { + transformed = char + } + result.WriteByte(transformed) + } + + return result.String() +} + +func getMmkv(udid string, id int) (name string, key string, err error) { + str1 := caesar(udid, id+3) + int1, err := strconv.ParseInt(udid[5:7], 16, 32) + if err != nil { + return "", "", fmt.Errorf("getMmkv: could not parse udid: %w", err) + } + int2 := 5 + (int(int1)+id)%4 + name = str1[:int2] + + int3 := id + 0xa546 + str3 := fmt.Sprintf("%s%04x", udid, int3) + hash := md5.Sum([]byte(str3)) + key = fmt.Sprintf("%x", hash)[:16] + + return name, key, nil +} + +func loadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Warn("Failed to get home dir") + return nil, fmt.Errorf("loadMacKeysV10: failed to get home: %w", err) + } + plist_path := filepath.Join( + homeDir, + "Library/Preferences/com.tencent.QQMusicMac.plist", + ) + if f, err := os.Stat(plist_path); err != nil || f.IsDir() { + logger.Debug("loadMacKeysV10: plist not found", zap.String("plist", plist_path)) + return nil, fmt.Errorf("loadMacKeysV10: plist not found") + } + plist_data, err := os.ReadFile(plist_path) + if err != nil { + logger.Warn("loadMacKeysV10: could not read plist", zap.String("plist", plist_path), zap.Error(err)) + return nil, fmt.Errorf("loadMacKeysV10: could not read plist: %w", err) + } + udid, err := extractUdid(plist_data) + if err != nil { + logger.Warn("loadMacKeysV10: could not extract udid", zap.Error(err)) + return nil, fmt.Errorf("loadMacKeysV10: could not extract udid: %w", err) + } + logger.Debug("loadMacKeysV10: read udid", zap.String("udid", udid)) + + mmkv_dir := filepath.Join( + homeDir, + "Library/Containers/com.tencent.QQMusicMac/Data/", + "Library/Application Support/QQMusicMac/iData", + ) + if f, err := os.Stat(mmkv_dir); err != nil || !f.IsDir() { + logger.Debug("loadMacKeysV10: mmkv dir not found", zap.String("mmkv_dir", mmkv_dir)) + return nil, nil + } + mmkv_name, mmkv_key, err := getMmkv(udid, 1) + if err != nil { + logger.Warn("loadMacKeysV10: could not get mmkv name/key", zap.Error(err)) + return nil, fmt.Errorf("loadMacKeysV10: could not get mmkv name/key: %w", err) + } + mmkv_path := filepath.Join(mmkv_dir, mmkv_name) + if f, err := os.Stat(mmkv_path); err != nil || f.IsDir() { + logger.Debug("loadMacKeysV10: mmkv file not found", zap.String("mmkv", mmkv_path)) + return nil, nil + } + logger.Info("Using QQMusic 10.x mmkv", zap.String("mmkv", mmkv_path)) + keys, err := LoadMMKV(mmkv_path, mmkv_key, logger) + if err != nil { + logger.Warn("loadMacKeysV10: could not load mmkv", zap.String("mmkv", mmkv_path), zap.Error(err)) + return nil, fmt.Errorf("loadMacKeysV10: could not load mmkv: %w", err) + } + return keys, nil } diff --git a/go.mod b/go.mod index 2ca29f6..7372238 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/go-flac v1.0.0 github.com/samber/lo v1.47.0 - github.com/unlock-music/go-mmkv v0.1.1 + github.com/unlock-music/go-mmkv v0.1.2 github.com/urfave/cli/v2 v2.27.5 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.29.0 diff --git a/go.sum b/go.sum index 07a1a5a..0327f04 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/unlock-music/go-mmkv v0.1.1 h1:1w3shjaBMp58jRrRkxPghnMpvyUoP4ZkA++W9ueFpE8= github.com/unlock-music/go-mmkv v0.1.1/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc= +github.com/unlock-music/go-mmkv v0.1.2 h1:nZvYoyO/LO4ycXHYmObgHVNXxqMjfINeCYwKFmiyOQc= +github.com/unlock-music/go-mmkv v0.1.2/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=