From a46428e07cb800df3bcdbd0472bc8d5b4b0473ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Tue, 9 Sep 2025 21:49:22 +0900 Subject: [PATCH] feat: support QQMusic from AppStore --- algo/qmc/key_mmkv_loader_darwin.go | 138 +-------------- algo/qmc/qmmac/v10_darwin.go | 163 ++++++++++++++++++ algo/qmc/qmmac/v8_darwin.go | 30 ++++ algo/qmc/key_mmkv.go => internal/mmkv/mmkv.go | 6 +- 4 files changed, 202 insertions(+), 135 deletions(-) create mode 100644 algo/qmc/qmmac/v10_darwin.go create mode 100644 algo/qmc/qmmac/v8_darwin.go rename algo/qmc/key_mmkv.go => internal/mmkv/mmkv.go (91%) diff --git a/algo/qmc/key_mmkv_loader_darwin.go b/algo/qmc/key_mmkv_loader_darwin.go index bcef203..e406c2c 100644 --- a/algo/qmc/key_mmkv_loader_darwin.go +++ b/algo/qmc/key_mmkv_loader_darwin.go @@ -1,26 +1,20 @@ package qmc import ( - "crypto/md5" - "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "git.um-react.app/um/cli/algo/common" + "git.um-react.app/um/cli/algo/qmc/qmmac" + "git.um-react.app/um/cli/internal/mmkv" "go.uber.org/zap" ) func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { - key1, err := loadMacKeysV8(logger) + key1, err := qmmac.LoadMacKeysV8(logger) if err != nil { key1 = nil logger.Warn("LoadMMKVOrDefault: could not read QQMusic v8.8.0 keys", zap.Error(err)) } - key2, err := loadMacKeysV10(logger) + key2, err := qmmac.LoadMacKeysV10(logger) if err != nil { key2 = nil logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err)) @@ -29,136 +23,16 @@ func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result comm userKeys := make(common.QMCKeys) if path != "" { logger.Info("Using user mmkv") - userKeys, err = LoadMMKV(path, key, logger) + userKeys, err = mmkv.LoadFromPath(path, key, logger) if err != nil { userKeys = nil logger.Warn("LoadMMKVOrDefault: could not read user keys", zap.Error(err)) } } - allKeys := mergeMMKVKeys(key1, key2, userKeys) + allKeys := mmkv.Merge(key1, key2, userKeys) logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys))) return allKeys, nil } - -func loadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) { - homeDir, err := os.UserHomeDir() - if err != nil { - logger.Warn("Failed to get home dir") - return nil, fmt.Errorf("loadMacKeysV8: failed to get home: %w", err) - } - p := filepath.Join( - homeDir, - "Library/Containers/com.tencent.QQMusicMac/Data", - "Library/Application Support/QQMusicMac/mmkv", - "MMKVStreamEncryptId", - ) - if f, err := os.Stat(p); err == nil && !f.IsDir() { - logger.Info("Using QQMusic 8.x mmkv", zap.String("mmkv", p)) - return LoadMMKV(p, "", 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/algo/qmc/qmmac/v10_darwin.go b/algo/qmc/qmmac/v10_darwin.go new file mode 100644 index 0000000..8f3facf --- /dev/null +++ b/algo/qmc/qmmac/v10_darwin.go @@ -0,0 +1,163 @@ +package qmmac + +import ( + "crypto/md5" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "git.um-react.app/um/cli/algo/common" + "git.um-react.app/um/cli/internal/mmkv" + "go.uber.org/zap" +) + +var _RE_UDID_V10 = regexp.MustCompile(`_\x10\(([0-9a-f]{40})_`) + +type QQMusicMacV10 struct { + logger *zap.Logger + mmkv_dir string +} + +func (q *QQMusicMacV10) extractUdids(data []byte) ([]string, error) { + var result []string + for _, match := range _RE_UDID_V10.FindAllSubmatch(data, -1) { + udid := string(match[1]) + q.logger.Debug("extractUdids: found udid", zap.String("udid", udid)) + result = append(result, udid) + } + return result, nil +} + +func (q *QQMusicMacV10) 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 (q *QQMusicMacV10) mmkv(udid string, id int) (path string, key string, err error) { + str1 := q.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] + path = filepath.Join(q.mmkv_dir, name) + + int3 := id + 0xa546 + str3 := fmt.Sprintf("%s%04x", udid, int3) + hash := md5.Sum([]byte(str3)) + key = fmt.Sprintf("%x", hash)[:16] + + return path, key, nil +} + +func (q *QQMusicMacV10) loadByPList(plist_path string) ([]common.QMCKeys, error) { + logger := q.logger.With(zap.String("plist", plist_path)) + logger.Debug("loadMacKeysV10: load key from plist") + if f, err := os.Stat(plist_path); err != nil || f.IsDir() { + logger.Debug("loadMacKeysV10: plist not found") + return nil, nil + } + plist_data, err := os.ReadFile(plist_path) + if err != nil { + logger.Warn("loadMacKeysV10: could not read plist", zap.Error(err)) + return nil, fmt.Errorf("loadMacKeysV10: could not read plist: %w", err) + } + udids, err := q.extractUdids(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.Strings("udids", udids)) + + var keysList []common.QMCKeys + for _, udid := range udids { + keys, err := q.loadByUDID(udid, logger) + if err != nil { + logger.Warn("loadMacKeysV10: could not load by udid", zap.String("udid", udid), zap.Error(err)) + continue + } + keysList = append(keysList, keys) + } + return keysList, nil +} +func (q *QQMusicMacV10) loadByUDID(udid string, logger *zap.Logger) (common.QMCKeys, error) { + mmkv_path, mmkv_key, err := q.mmkv(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) + } + logger.Info("Using QQMusic 10.x mmkv", zap.String("mmkv", mmkv_path)) + keys, err := mmkv.LoadFromPath(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 +} + +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) + } + + // MMKV dir is always inside the sandbox container + 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 + } + + // without sandbox + plist_without_sandbox := filepath.Join( + homeDir, + "Library/Preferences/com.tencent.QQMusicMac.plist", + ) + + // with sandbox (e.g. from App Store) + plist_sandboxed := filepath.Join( + homeDir, + "Library/Containers/com.tencent.QQMusicMac/Data/", + "Library/Preferences/com.tencent.QQMusicMac.plist", + ) + + q := QQMusicMacV10{ + logger: logger, + mmkv_dir: mmkv_dir, + } + + keys1, err := q.loadByPList(plist_without_sandbox) + if err != nil { + return nil, err + } + keys2, err := q.loadByPList(plist_sandboxed) + if err != nil { + return nil, err + } + + return mmkv.Merge(append(keys1, keys2...)...), nil +} diff --git a/algo/qmc/qmmac/v8_darwin.go b/algo/qmc/qmmac/v8_darwin.go new file mode 100644 index 0000000..9f6e45e --- /dev/null +++ b/algo/qmc/qmmac/v8_darwin.go @@ -0,0 +1,30 @@ +package qmmac + +import ( + "fmt" + "os" + "path/filepath" + + "git.um-react.app/um/cli/algo/common" + "git.um-react.app/um/cli/internal/mmkv" + "go.uber.org/zap" +) + +func LoadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) { + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Warn("Failed to get home dir") + return nil, fmt.Errorf("loadMacKeysV8: failed to get home: %w", err) + } + p := filepath.Join( + homeDir, + "Library/Containers/com.tencent.QQMusicMac/Data", + "Library/Application Support/QQMusicMac/mmkv", + "MMKVStreamEncryptId", + ) + if f, err := os.Stat(p); err == nil && !f.IsDir() { + logger.Info("Using QQMusic 8.x mmkv", zap.String("mmkv", p)) + return mmkv.LoadFromPath(p, "", logger) + } + return nil, nil +} diff --git a/algo/qmc/key_mmkv.go b/internal/mmkv/mmkv.go similarity index 91% rename from algo/qmc/key_mmkv.go rename to internal/mmkv/mmkv.go index 1ed941f..0570f8c 100644 --- a/algo/qmc/key_mmkv.go +++ b/internal/mmkv/mmkv.go @@ -1,4 +1,4 @@ -package qmc +package mmkv import ( "fmt" @@ -10,7 +10,7 @@ import ( "go.uber.org/zap" ) -func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys { +func Merge(keys ...common.QMCKeys) common.QMCKeys { result := make(common.QMCKeys) for _, k := range keys { for key, value := range k { @@ -20,7 +20,7 @@ func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys { return result } -func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { +func LoadFromPath(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { mmkv_path := path mmkv_crc := path + ".crc"