mirror of
https://git.um-react.app/um/cli.git
synced 2025-11-28 03:33:02 +00:00
refactor: improve mmkv logic
This commit is contained in:
@@ -6,8 +6,24 @@ import (
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"unlock-music.dev/cli/internal/utils"
|
||||
)
|
||||
|
||||
type QMCKeys map[string]string
|
||||
|
||||
type CryptoParams struct {
|
||||
// KuGou kgg database path
|
||||
KggDbPath string
|
||||
|
||||
// QMC Crypto config
|
||||
QmcKeys QMCKeys
|
||||
}
|
||||
|
||||
func (k QMCKeys) Get(key string) (string, bool) {
|
||||
value, ok := k[utils.NormalizeUnicode(key)]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
type DecoderParams struct {
|
||||
Reader io.ReadSeeker // required
|
||||
Extension string // required, source extension, eg. ".mp3"
|
||||
@@ -16,8 +32,7 @@ type DecoderParams struct {
|
||||
|
||||
Logger *zap.Logger // required
|
||||
|
||||
// KuGou
|
||||
KggDatabasePath string
|
||||
CryptoParams CryptoParams
|
||||
}
|
||||
type NewDecoderFunc func(p *DecoderParams) Decoder
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type Decoder struct {
|
||||
}
|
||||
|
||||
func NewDecoder(p *common.DecoderParams) common.Decoder {
|
||||
return &Decoder{rd: p.Reader, KggDatabasePath: p.KggDatabasePath}
|
||||
return &Decoder{rd: p.Reader, KggDatabasePath: p.CryptoParams.KggDbPath}
|
||||
}
|
||||
|
||||
// Validate checks if the file is a valid Kugou (.kgm, .vpr, .kgma) file.
|
||||
|
||||
@@ -1,118 +1,75 @@
|
||||
package qmc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/samber/lo"
|
||||
mmkv "github.com/unlock-music/go-mmkv"
|
||||
go_mmkv "github.com/unlock-music/go-mmkv"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
"unlock-music.dev/cli/internal/utils"
|
||||
)
|
||||
|
||||
var streamKeyVault mmkv.Vault
|
||||
|
||||
// TODO: move to factory
|
||||
func readKeyFromMMKV(file string, logger *zap.Logger) ([]byte, error) {
|
||||
if file == "" {
|
||||
return nil, errors.New("file path is required while reading key from mmkv")
|
||||
}
|
||||
|
||||
//goland:noinspection GoBoolExpressions
|
||||
if runtime.GOOS != "darwin" {
|
||||
return nil, errors.New("mmkv vault not supported on this platform")
|
||||
}
|
||||
|
||||
if streamKeyVault == nil {
|
||||
mmkvDir, err := getRelativeMMKVDir(file)
|
||||
if err != nil {
|
||||
mmkvDir, err = getDefaultMMKVDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mmkv key valut not found: %w", err)
|
||||
}
|
||||
func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys {
|
||||
result := make(common.QMCKeys)
|
||||
for _, k := range keys {
|
||||
for key, value := range k {
|
||||
result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value)
|
||||
}
|
||||
|
||||
mgr, err := mmkv.NewManager(mmkvDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init mmkv manager: %w", err)
|
||||
}
|
||||
|
||||
streamKeyVault, err = mgr.OpenVault("MMKVStreamEncryptId")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open mmkv vault: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys()))
|
||||
}
|
||||
|
||||
_, partName := filepath.Split(file)
|
||||
partName = normalizeUnicode(partName)
|
||||
|
||||
buf, err := streamKeyVault.GetBytes(file)
|
||||
if buf == nil {
|
||||
filePaths := streamKeyVault.Keys()
|
||||
fileNames := lo.Map(filePaths, func(filePath string, _ int) string {
|
||||
_, name := filepath.Split(filePath)
|
||||
return normalizeUnicode(name)
|
||||
})
|
||||
|
||||
for _, key := range fileNames { // fallback: match filename only
|
||||
if key != partName {
|
||||
continue
|
||||
}
|
||||
idx := slices.Index(fileNames, key)
|
||||
buf, err = streamKeyVault.GetBytes(filePaths[idx])
|
||||
if err != nil {
|
||||
logger.Warn("read key from mmkv", zap.String("key", filePaths[idx]), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil, errors.New("key not found in mmkv vault")
|
||||
}
|
||||
|
||||
return deriveKey(buf)
|
||||
return result
|
||||
}
|
||||
|
||||
func OpenMMKV(mmkvPath string, key string, logger *zap.Logger) error {
|
||||
filePath, fileName := filepath.Split(mmkvPath)
|
||||
mgr, err := mmkv.NewManager(filepath.Dir(filePath))
|
||||
func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
mmkv_path := path
|
||||
mmkv_crc := path + ".crc"
|
||||
|
||||
mr, err := os.Open(mmkv_path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init mmkv manager: %w", err)
|
||||
logger.Error("LoadMMKV: Could not open mmkv file", zap.Error(err))
|
||||
return nil, fmt.Errorf("LoadMMKV: open error: %w", err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
cr, err := os.Open(mmkv_crc)
|
||||
if err == nil {
|
||||
// crc is optional
|
||||
cr = nil
|
||||
defer cr.Close()
|
||||
}
|
||||
|
||||
// If `vaultKey` is empty, the key is ignored.
|
||||
streamKeyVault, err = mgr.OpenVaultCrypto(fileName, key)
|
||||
|
||||
var password []byte = nil
|
||||
if key != "" {
|
||||
password = make([]byte, 16)
|
||||
copy(password, []byte(key))
|
||||
}
|
||||
mmkv, err := go_mmkv.NewMMKVReader(mr, password, cr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open mmkv vault: %w", err)
|
||||
logger.Error("LoadMMKV: failed to create reader", zap.Error(err))
|
||||
return nil, fmt.Errorf("LoadMMKV: NewMMKVReader error: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys()))
|
||||
return nil
|
||||
result = make(common.QMCKeys)
|
||||
for !mmkv.IsEOF() {
|
||||
key, err := mmkv.ReadKey()
|
||||
if err != nil {
|
||||
logger.Error("LoadMMKV: read key error", zap.Error(err))
|
||||
return nil, fmt.Errorf("LoadMMKV: read key error: %w", err)
|
||||
}
|
||||
value, err := mmkv.ReadStringValue()
|
||||
if err != nil {
|
||||
logger.Error("LoadMMKV: read value error", zap.Error(err))
|
||||
return nil, fmt.Errorf("LoadMMKV: read value error: %w", err)
|
||||
}
|
||||
logger.Debug("LoadMMKV: read", zap.String("key", key), zap.String("value", value))
|
||||
result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// /
|
||||
func readKeyFromMMKVCustom(mid string) ([]byte, error) {
|
||||
if streamKeyVault == nil {
|
||||
return nil, fmt.Errorf("mmkv vault not loaded")
|
||||
}
|
||||
|
||||
// get ekey from mmkv vault
|
||||
eKey, err := streamKeyVault.GetBytes(mid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get eKey error: %w", err)
|
||||
}
|
||||
return deriveKey(eKey)
|
||||
}
|
||||
|
||||
// / getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour)
|
||||
// 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 {
|
||||
@@ -149,10 +106,3 @@ func getDefaultMMKVDir() (string, error) {
|
||||
|
||||
return mmkvDir, nil
|
||||
}
|
||||
|
||||
// normalizeUnicode normalizes unicode string to NFC.
|
||||
// since macOS may change some characters in the file name.
|
||||
// e.g. "ぜ"(e3 81 9c) -> "ぜ"(e3 81 9b e3 82 99)
|
||||
func normalizeUnicode(str string) string {
|
||||
return norm.NFC.String(str)
|
||||
}
|
||||
|
||||
65
algo/qmc/key_mmkv_loader_darwin.go
Normal file
65
algo/qmc/key_mmkv_loader_darwin.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package qmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
)
|
||||
|
||||
func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
key1, err := 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)
|
||||
if err != nil {
|
||||
key2 = nil
|
||||
logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err))
|
||||
}
|
||||
|
||||
userKeys := make(common.QMCKeys)
|
||||
if path != "" {
|
||||
logger.Info("Using user mmkv")
|
||||
userKeys, err = LoadMMKV(path, key, logger)
|
||||
if err != nil {
|
||||
userKeys = nil
|
||||
logger.Warn("LoadMMKVOrDefault: could not read user keys", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
allKeys := mergeMMKVKeys(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
|
||||
}
|
||||
|
||||
func loadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) {
|
||||
// TODO: stub only
|
||||
var _ = logger
|
||||
return nil, nil
|
||||
}
|
||||
13
algo/qmc/key_mmkv_loader_default.go
Normal file
13
algo/qmc/key_mmkv_loader_default.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !darwin
|
||||
|
||||
package qmc
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
)
|
||||
|
||||
func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
// Stub: do nothing
|
||||
return nil, nil
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -133,14 +132,15 @@ func (d *Decoder) searchKey() (err error) {
|
||||
}
|
||||
fileSize := int(fileSizeM4) + 4
|
||||
|
||||
//goland:noinspection GoBoolExpressions
|
||||
if runtime.GOOS == "darwin" && !strings.HasPrefix(d.params.Extension, ".qmc") {
|
||||
d.decodedKey, err = readKeyFromMMKV(d.params.FilePath, d.logger)
|
||||
if key, ok := d.params.CryptoParams.QmcKeys.Get(d.params.FilePath); ok {
|
||||
d.logger.Debug("QQMusic Mac Legacy file", zap.String("file", d.params.FilePath), zap.String("key", key))
|
||||
d.decodedKey, err = deriveKey([]byte(key))
|
||||
if err == nil {
|
||||
d.audioLen = fileSize
|
||||
return
|
||||
return nil
|
||||
}
|
||||
d.logger.Warn("read key from mmkv failed", zap.Error(err))
|
||||
d.decodedKey = nil
|
||||
d.logger.Warn("could not derive key, skip", zap.Error(err))
|
||||
}
|
||||
|
||||
suffixBuf := make([]byte, 4)
|
||||
@@ -153,17 +153,17 @@ func (d *Decoder) searchKey() (err error) {
|
||||
return d.readRawMetaQTag()
|
||||
case "STag":
|
||||
return errors.New("qmc: file with 'STag' suffix doesn't contains media key")
|
||||
// MusicEx\0
|
||||
case "cex\x00":
|
||||
footer, err := NewMusicExTag(d.raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.audioLen = fileSize - int(footer.TagSize)
|
||||
d.decodedKey, err = readKeyFromMMKVCustom(footer.MediaFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
if key, ok := d.params.CryptoParams.QmcKeys.Get(footer.MediaFileName); ok {
|
||||
d.decodedKey, err = deriveKey([]byte(key))
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
default:
|
||||
size := binary.LittleEndian.Uint32(suffixBuf)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user