feat: QQMusic Mac v10 mmkv support

This commit is contained in:
鲁树人
2025-09-08 23:30:27 +09:00
parent 1b62887619
commit e06a21123f
4 changed files with 110 additions and 46 deletions

View File

@@ -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
}

View File

@@ -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
}
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) {
// TODO: stub only
var _ = logger
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
}

2
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=