Merge pull request '小更改 - mmkv 相关' (#93) from qmc-mmkv-cipher into master

Reviewed-on: https://git.unlock-music.dev/um/cli/pulls/93
pull/95/head v0.2.0-beta.3
鲁树人 5 months ago
commit 05a1affa03

@ -80,14 +80,16 @@ func readKeyFromMMKV(file string, logger *zap.Logger) ([]byte, error) {
return deriveKey(buf) return deriveKey(buf)
} }
func OpenMMKV(vaultPath string, vaultKey string, logger *zap.Logger) error { func OpenMMKV(mmkvPath string, key string, logger *zap.Logger) error {
filePath, fileName := filepath.Split(vaultPath) filePath, fileName := filepath.Split(mmkvPath)
mgr, err := mmkv.NewManager(filepath.Dir(filePath)) mgr, err := mmkv.NewManager(filepath.Dir(filePath))
if err != nil { if err != nil {
return fmt.Errorf("init mmkv manager: %w", err) return fmt.Errorf("init mmkv manager: %w", err)
} }
streamKeyVault, err = mgr.OpenVaultCrypto(fileName, vaultKey) // If `vaultKey` is empty, the key is ignored.
streamKeyVault, err = mgr.OpenVaultCrypto(fileName, key)
if err != nil { if err != nil {
return fmt.Errorf("open mmkv vault: %w", err) return fmt.Errorf("open mmkv vault: %w", err)
} }
@ -96,6 +98,7 @@ func OpenMMKV(vaultPath string, vaultKey string, logger *zap.Logger) error {
return nil return nil
} }
// /
func readKeyFromMMKVCustom(mid string) ([]byte, error) { func readKeyFromMMKVCustom(mid string) ([]byte, error) {
if streamKeyVault == nil { if streamKeyVault == nil {
return nil, fmt.Errorf("mmkv vault not loaded") return nil, fmt.Errorf("mmkv vault not loaded")
@ -109,6 +112,7 @@ func readKeyFromMMKVCustom(mid string) ([]byte, error) {
return deriveKey(eKey) return deriveKey(eKey)
} }
// / getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour)
func getRelativeMMKVDir(file string) (string, error) { func getRelativeMMKVDir(file string) (string, error) {
mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv") mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv")
if _, err := os.Stat(mmkvDir); err != nil { if _, err := os.Stat(mmkvDir); err != nil {
@ -131,7 +135,7 @@ func getDefaultMMKVDir() (string, error) {
mmkvDir := filepath.Join( mmkvDir := filepath.Join(
homeDir, homeDir,
"Library/Containers/com.tencent.QQMusicMac/Data", // todo: make configurable "Library/Containers/com.tencent.QQMusicMac/Data",
"Library/Application Support/QQMusicMac/mmkv", "Library/Application Support/QQMusicMac/mmkv",
) )
if _, err := os.Stat(mmkvDir); err != nil { if _, err := os.Stat(mmkvDir); err != nil {

@ -145,13 +145,12 @@ func (d *Decoder) searchKey() (err error) {
case "STag": case "STag":
return errors.New("qmc: file with 'STag' suffix doesn't contains media key") return errors.New("qmc: file with 'STag' suffix doesn't contains media key")
case "cex\x00": case "cex\x00":
footer := qqMusicTagMusicEx{} footer, err := NewMusicExTag(d.raw)
audioLen, err := footer.Read(d.raw)
if err != nil { if err != nil {
return err return err
} }
d.audioLen = int(audioLen) d.audioLen = fileSize - int(footer.TagSize)
d.decodedKey, err = readKeyFromMMKVCustom(footer.mediafile) d.decodedKey, err = readKeyFromMMKVCustom(footer.MediaFileName)
if err != nil { if err != nil {
return err return err
} }

@ -1,65 +1,93 @@
package qmc package qmc
import ( import (
bytes "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"io" "io"
"strings"
) )
type qqMusicTagMusicEx struct { type MusicExTagV1 struct {
songid uint32 // Song ID SongID uint32 // Song ID
unknown_1 uint32 // unused & unknown Unknown1 uint32 // unused & unknown
unknown_2 uint32 // unused & unknown Unknown2 uint32 // unused & unknown
mid string // Media ID MediaID string // Media ID
mediafile string // real file name MediaFileName string // real file name
unknown_3 uint32 // unused; uninitialized memory? Unknown3 uint32 // unused; uninitialized memory?
sizeof_struct uint32 // 19.57: fixed value: 0xC0
version uint32 // 19.57: fixed value: 0x01 // 16 byte at the end of tag.
tag_magic []byte // fixed value "musicex\0" (8 bytes) // TagSize should be respected when parsing.
TagSize uint32 // 19.57: fixed value: 0xC0
TagVersion uint32 // 19.57: fixed value: 0x01
TagMagic []byte // fixed value "musicex\0" (8 bytes)
} }
func (tag *qqMusicTagMusicEx) Read(raw io.ReadSeeker) (int64, error) { func NewMusicExTag(f io.ReadSeeker) (*MusicExTagV1, error) {
_, err := raw.Seek(-16, io.SeekEnd) _, err := f.Seek(-16, io.SeekEnd)
if err != nil { if err != nil {
return 0, fmt.Errorf("musicex seek error: %w", err) return nil, fmt.Errorf("musicex seek error: %w", err)
} }
footerBuf := make([]byte, 4) buffer := make([]byte, 16)
footerBuf, err = io.ReadAll(io.LimitReader(raw, 4)) bytesRead, err := f.Read(buffer)
if err != nil { if err != nil {
return 0, fmt.Errorf("get musicex error: %w", err) return nil, fmt.Errorf("get musicex error: %w", err)
}
if bytesRead != 16 {
return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, 16)
} }
footerLen := int64(binary.LittleEndian.Uint32(footerBuf))
audioLen, err := raw.Seek(-footerLen, io.SeekEnd) tag := &MusicExTagV1{
buf, err := io.ReadAll(io.LimitReader(raw, audioLen)) TagSize: binary.LittleEndian.Uint32(buffer[0x00:0x04]),
if err != nil { TagVersion: binary.LittleEndian.Uint32(buffer[0x04:0x08]),
return 0, err TagMagic: buffer[0x04:0x0C],
} }
tag.songid = binary.LittleEndian.Uint32(buf[0:4]) if !bytes.Equal(tag.TagMagic, []byte("musicex\x00")) {
tag.unknown_1 = binary.LittleEndian.Uint32(buf[4:8]) return nil, errors.New("MusicEx magic mismatch")
tag.unknown_2 = binary.LittleEndian.Uint32(buf[8:12]) }
if tag.TagVersion != 1 {
return nil, errors.New(fmt.Sprintf("unsupported musicex tag version. expecting 1, got %d", tag.TagVersion))
}
for i := 0; i < 30; i++ { if tag.TagSize < 0xC0 {
u := binary.LittleEndian.Uint16(buf[12+i*2 : 12+(i+1)*2]) return nil, errors.New(fmt.Sprintf("unsupported musicex tag size. expecting at least 0xC0, got 0x%02x", tag.TagSize))
if u == 0 { }
break
} buffer = make([]byte, tag.TagSize)
tag.mid += string(u) bytesRead, err = f.Read(buffer)
if err != nil {
return nil, err
} }
for i := 0; i < 50; i++ { if uint32(bytesRead) != tag.TagSize {
u := binary.LittleEndian.Uint16(buf[72+i*2 : 72+(i+1)*2]) return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, tag.TagSize)
if u == 0 { }
tag.SongID = binary.LittleEndian.Uint32(buffer[0x00:0x04])
tag.Unknown1 = binary.LittleEndian.Uint32(buffer[0x04:0x08])
tag.Unknown2 = binary.LittleEndian.Uint32(buffer[0x08:0x0C])
tag.MediaID = readUnicodeTagName(buffer[0x0C:], 30*2)
tag.MediaFileName = readUnicodeTagName(buffer[0x48:], 50*2)
tag.Unknown3 = binary.LittleEndian.Uint32(buffer[0xAC:0xB0])
return tag, nil
}
// readUnicodeTagName reads a buffer to maxLen.
// reconstruct text by skipping alternate char (ascii chars encoded in UTF-16-LE),
// until finding a zero or reaching maxLen.
func readUnicodeTagName(buffer []byte, maxLen int) string {
builder := strings.Builder{}
for i := 0; i < maxLen; i += 2 {
chr := buffer[i]
if chr != 0 {
builder.WriteByte(chr)
} else {
break break
} }
tag.mediafile += string(u)
} }
tag.unknown_3 = binary.LittleEndian.Uint32(buf[173:177]) return builder.String()
tag.sizeof_struct = binary.LittleEndian.Uint32(buf[177:181])
tag.version = binary.LittleEndian.Uint32(buf[181:185])
tag.tag_magic = buf[185:193]
return audioLen, nil
} }

@ -5,9 +5,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/fsnotify/fsnotify"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"io" "io"
"os" "os"
"os/signal" "os/signal"
@ -18,6 +15,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/fsnotify/fsnotify"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"unlock-music.dev/cli/algo/common" "unlock-music.dev/cli/algo/common"
_ "unlock-music.dev/cli/algo/kgm" _ "unlock-music.dev/cli/algo/kgm"
_ "unlock-music.dev/cli/algo/kwm" _ "unlock-music.dev/cli/algo/kwm"
@ -49,8 +50,8 @@ func main() {
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false}, &cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false}, &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false},
&cli.StringFlag{Name: "vault-file", Aliases: []string{"db"}, Usage: "数据库文件位置 (请确保crc文件在同目录下)", Required: false}, &cli.StringFlag{Name: "qmc-mmkv", Aliases: []string{"db"}, Usage: "path to qmc mmkv (`.crc` file also required)", Required: false},
&cli.StringFlag{Name: "vault-key", Aliases: []string{"key"}, Usage: "数据库密钥 (length 32)", Required: false}, &cli.StringFlag{Name: "qmc-mmkv-key", Aliases: []string{"key"}, Usage: "mmkv password (16 ascii chars)", Required: false},
&cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false}, &cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false},
&cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true}, &cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true},
&cli.BoolFlag{Name: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false}, &cli.BoolFlag{Name: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false},
@ -131,10 +132,10 @@ func appMain(c *cli.Context) (err error) {
return errors.New("output should be a writable directory") return errors.New("output should be a writable directory")
} }
vaultPath := c.String("vault-file") if mmkv := c.String("qmc-mmkv"); mmkv != "" {
vaultKey := c.String("vault-key") // If key is not set, the mmkv vault will be treated as unencrypted.
if vaultPath != "" && vaultKey != "" { key := c.String("qmc-mmkv-key")
err := qmc.OpenMMKV(vaultPath, vaultKey, logger) err := qmc.OpenMMKV(mmkv, key, logger)
if err != nil { if err != nil {
return err return err
} }

Loading…
Cancel
Save