diff --git a/algo/qmc/key_mmkv.go b/algo/qmc/key_mmkv.go index 3dff9eb..24b51cb 100644 --- a/algo/qmc/key_mmkv.go +++ b/algo/qmc/key_mmkv.go @@ -80,6 +80,35 @@ func readKeyFromMMKV(file string, logger *zap.Logger) ([]byte, error) { return deriveKey(buf) } +func OpenMMKV(vaultPath string, vaultKey string, logger *zap.Logger) error { + filePath, fileName := filepath.Split(vaultPath) + mgr, err := mmkv.NewManager(filepath.Dir(filePath)) + if err != nil { + return fmt.Errorf("init mmkv manager: %w", err) + } + + streamKeyVault, err = mgr.OpenVaultCrypto(fileName, vaultKey) + if err != nil { + return fmt.Errorf("open mmkv vault: %w", err) + } + + logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys())) + return 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) +} + func getRelativeMMKVDir(file string) (string, error) { mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv") if _, err := os.Stat(mmkvDir); err != nil { diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index d6c084d..310e642 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -5,13 +5,12 @@ import ( "encoding/binary" "errors" "fmt" + "go.uber.org/zap" "io" "runtime" "strconv" "strings" - "go.uber.org/zap" - "unlock-music.dev/cli/algo/common" "unlock-music.dev/cli/internal/sniff" ) @@ -145,6 +144,18 @@ func (d *Decoder) searchKey() (err error) { return d.readRawMetaQTag() case "STag": return errors.New("qmc: file with 'STag' suffix doesn't contains media key") + case "cex\x00": + footer := qqMusicTagMusicEx{} + audioLen, err := footer.Read(d.raw) + if err != nil { + return err + } + d.audioLen = int(audioLen) + d.decodedKey, err = readKeyFromMMKVCustom(footer.mediafile) + if err != nil { + return err + } + return nil default: size := binary.LittleEndian.Uint32(suffixBuf) diff --git a/algo/qmc/qmc_footer_musicex.go b/algo/qmc/qmc_footer_musicex.go new file mode 100644 index 0000000..444c2aa --- /dev/null +++ b/algo/qmc/qmc_footer_musicex.go @@ -0,0 +1,65 @@ +package qmc + +import ( + "encoding/binary" + "fmt" + "io" +) + +type qqMusicTagMusicEx struct { + songid uint32 // Song ID + unknown_1 uint32 // unused & unknown + unknown_2 uint32 // unused & unknown + mid string // Media ID + mediafile string // real file name + unknown_3 uint32 // unused; uninitialized memory? + sizeof_struct uint32 // 19.57: fixed value: 0xC0 + version uint32 // 19.57: fixed value: 0x01 + tag_magic []byte // fixed value "musicex\0" (8 bytes) +} + +func (tag *qqMusicTagMusicEx) Read(raw io.ReadSeeker) (int64, error) { + _, err := raw.Seek(-16, io.SeekEnd) + if err != nil { + return 0, fmt.Errorf("musicex seek error: %w", err) + } + + footerBuf := make([]byte, 4) + footerBuf, err = io.ReadAll(io.LimitReader(raw, 4)) + if err != nil { + return 0, fmt.Errorf("get musicex error: %w", err) + } + footerLen := int64(binary.LittleEndian.Uint32(footerBuf)) + + audioLen, err := raw.Seek(-footerLen, io.SeekEnd) + buf, err := io.ReadAll(io.LimitReader(raw, audioLen)) + if err != nil { + return 0, err + } + + tag.songid = binary.LittleEndian.Uint32(buf[0:4]) + tag.unknown_1 = binary.LittleEndian.Uint32(buf[4:8]) + tag.unknown_2 = binary.LittleEndian.Uint32(buf[8:12]) + + for i := 0; i < 30; i++ { + u := binary.LittleEndian.Uint16(buf[12+i*2 : 12+(i+1)*2]) + if u == 0 { + break + } + tag.mid += string(u) + } + for i := 0; i < 50; i++ { + u := binary.LittleEndian.Uint16(buf[72+i*2 : 72+(i+1)*2]) + if u == 0 { + break + } + tag.mediafile += string(u) + } + + tag.unknown_3 = binary.LittleEndian.Uint32(buf[173:177]) + 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 +} diff --git a/cmd/um/main.go b/cmd/um/main.go index 8402142..4b74ce7 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -5,6 +5,9 @@ import ( "context" "errors" "fmt" + "github.com/fsnotify/fsnotify" + "github.com/urfave/cli/v2" + "go.uber.org/zap" "io" "os" "os/signal" @@ -15,15 +18,11 @@ import ( "strings" "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/kgm" _ "unlock-music.dev/cli/algo/kwm" _ "unlock-music.dev/cli/algo/ncm" - _ "unlock-music.dev/cli/algo/qmc" + "unlock-music.dev/cli/algo/qmc" _ "unlock-music.dev/cli/algo/tm" _ "unlock-music.dev/cli/algo/xiami" _ "unlock-music.dev/cli/algo/ximalaya" @@ -50,6 +49,8 @@ func main() { Flags: []cli.Flag{ &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: "vault-file", Aliases: []string{"db"}, Usage: "数据库文件位置 (请确保crc文件在同目录下)", Required: false}, + &cli.StringFlag{Name: "vault-key", Aliases: []string{"key"}, Usage: "数据库密钥 (length 32)", Required: 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: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false}, @@ -130,6 +131,15 @@ func appMain(c *cli.Context) (err error) { return errors.New("output should be a writable directory") } + vaultPath := c.String("vault-file") + vaultKey := c.String("vault-key") + if vaultPath != "" && vaultKey != "" { + err := qmc.OpenMMKV(vaultPath, vaultKey, logger) + if err != nil { + return err + } + } + proc := &processor{ outputDir: output, skipNoopDecoder: c.Bool("skip-noop"),