diff --git a/algo/common/common.go b/algo/common/common.go index a248284..1bbb7a3 100644 --- a/algo/common/common.go +++ b/algo/common/common.go @@ -1,13 +1,13 @@ package common -import "context" +import ( + "context" + "io" +) type Decoder interface { Validate() error - Decode() error - GetAudioData() []byte - GetAudioExt() string - GetMeta() Meta + io.Reader } type CoverImageGetter interface { @@ -19,3 +19,7 @@ type Meta interface { GetTitle() string GetAlbum() string } + +type StreamDecoder interface { + Decrypt(buf []byte, offset int) +} diff --git a/algo/common/dispatch.go b/algo/common/dispatch.go index 71ce298..07b110c 100644 --- a/algo/common/dispatch.go +++ b/algo/common/dispatch.go @@ -1,11 +1,12 @@ package common import ( + "io" "path/filepath" "strings" ) -type NewDecoderFunc func([]byte) Decoder +type NewDecoderFunc func(rd io.ReadSeeker) Decoder type decoderItem struct { noop bool diff --git a/algo/common/raw.go b/algo/common/raw.go index 77bc043..5f4e9e6 100644 --- a/algo/common/raw.go +++ b/algo/common/raw.go @@ -2,21 +2,32 @@ package common import ( "errors" + "fmt" + "io" "strings" ) type RawDecoder struct { - file []byte + rd io.ReadSeeker + audioExt string } -func NewRawDecoder(file []byte) Decoder { - return &RawDecoder{file: file} +func NewRawDecoder(rd io.ReadSeeker) Decoder { + return &RawDecoder{rd: rd} } func (d *RawDecoder) Validate() error { + header := make([]byte, 16) + if _, err := io.ReadFull(d.rd, header); err != nil { + return fmt.Errorf("read file header failed: %v", err) + } + if _, err := d.rd.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("seek file failed: %v", err) + } + for ext, sniffer := range snifferRegistry { - if sniffer(d.file) { + if sniffer(header) { d.audioExt = strings.ToLower(ext) return nil } @@ -24,20 +35,8 @@ func (d *RawDecoder) Validate() error { return errors.New("audio doesn't recognized") } -func (d RawDecoder) Decode() error { - return nil -} - -func (d RawDecoder) GetAudioData() []byte { - return d.file -} - -func (d RawDecoder) GetAudioExt() string { - return d.audioExt -} - -func (d RawDecoder) GetMeta() Meta { - return nil +func (d *RawDecoder) Read(p []byte) (n int, err error) { + return d.rd.Read(p) } func init() { diff --git a/algo/kgm/kgm.go b/algo/kgm/kgm.go index 00e7098..7dcab20 100644 --- a/algo/kgm/kgm.go +++ b/algo/kgm/kgm.go @@ -1,7 +1,6 @@ package kgm import ( - "bytes" "fmt" "io" @@ -9,64 +8,48 @@ import ( ) type Decoder struct { - header Header - initializer kgmCryptoInitializer + header header + cipher common.StreamDecoder - file []byte - audio []byte + rd io.ReadSeeker + offset int } -type kgmCryptoInitializer func(header *Header, body io.Reader) (io.Reader, error) +func NewDecoder(rd io.ReadSeeker) common.Decoder { + return &Decoder{rd: rd} +} -var kgmCryptoInitializers = map[uint32]kgmCryptoInitializer{ +var kgmCryptoInitializers = map[uint32]func(header *header) (common.StreamDecoder, error){ 3: newKgmCryptoV3, } -func NewDecoder(file []byte) common.Decoder { - return &Decoder{ - file: file, - } -} - -func (d *Decoder) GetAudioData() []byte { - return d.audio -} - -func (d *Decoder) GetAudioExt() string { - return "" // use sniffer -} - -func (d *Decoder) GetMeta() common.Meta { - return nil -} - func (d *Decoder) Validate() error { - if err := d.header.FromBytes(d.file); err != nil { + if err := d.header.FromFile(d.rd); err != nil { return err } // TODO; validate crypto version - var ok bool - d.initializer, ok = kgmCryptoInitializers[d.header.CryptoVersion] + initializer, ok := kgmCryptoInitializers[d.header.CryptoVersion] if !ok { return fmt.Errorf("kgm: unsupported crypto version %d", d.header.CryptoVersion) } + var err error + d.cipher, err = initializer(&d.header) + if err != nil { + return fmt.Errorf("kgm: failed to initialize crypto: %w", err) + } + return nil } -func (d *Decoder) Decode() error { - d.audio = d.file[d.header.AudioOffset:] - - r, err := d.initializer(&d.header, bytes.NewReader(d.audio)) - if err != nil { - return fmt.Errorf("kgm: failed to initialize crypto: %w", err) +func (d *Decoder) Read(buf []byte) (int, error) { + n, err := d.rd.Read(buf) + if n > 0 { + d.cipher.Decrypt(buf[:n], d.offset) + d.offset += n } - d.audio, err = io.ReadAll(r) - if err != nil { - return fmt.Errorf("kgm: failed to decrypt audio: %w", err) - } - return nil + return n, err } func init() { diff --git a/algo/kgm/kgm_header.go b/algo/kgm/kgm_header.go index f7c6539..7eb8e30 100644 --- a/algo/kgm/kgm_header.go +++ b/algo/kgm/kgm_header.go @@ -21,8 +21,8 @@ var ( ErrKgmMagicHeader = errors.New("kgm magic header not matched") ) -// Header is the header of a KGM file. -type Header struct { +// header is the header of a KGM file. +type header struct { MagicHeader []byte // 0x00-0x0f: magic header AudioOffset uint32 // 0x10-0x13: offset of audio data CryptoVersion uint32 // 0x14-0x17: crypto version @@ -31,7 +31,7 @@ type Header struct { CryptoKey []byte // 0x2c-0x3b: crypto key } -func (h *Header) FromFile(rd io.ReadSeeker) error { +func (h *header) FromFile(rd io.ReadSeeker) error { if _, err := rd.Seek(0, io.SeekStart); err != nil { return fmt.Errorf("kgm seek start: %w", err) } @@ -44,7 +44,7 @@ func (h *Header) FromFile(rd io.ReadSeeker) error { return h.FromBytes(buf) } -func (h *Header) FromBytes(buf []byte) error { +func (h *header) FromBytes(buf []byte) error { if len(buf) < 0x3c { return errors.New("invalid kgm header length") } diff --git a/algo/kgm/kgm_v3.go b/algo/kgm/kgm_v3.go index 1a04c23..38173bc 100644 --- a/algo/kgm/kgm_v3.go +++ b/algo/kgm/kgm_v3.go @@ -3,24 +3,22 @@ package kgm import ( "crypto/md5" "fmt" - "io" + + "github.com/unlock-music/cli/algo/common" ) // kgmCryptoV3 is kgm file crypto v3 type kgmCryptoV3 struct { slotBox []byte fileBox []byte - - rd io.Reader - offset int } var kgmV3Slot2Key = map[uint32][]byte{ 1: {0x6C, 0x2C, 0x2F, 0x27}, } -func newKgmCryptoV3(header *Header, body io.Reader) (io.Reader, error) { - c := &kgmCryptoV3{rd: body} +func newKgmCryptoV3(header *header) (common.StreamDecoder, error) { + c := &kgmCryptoV3{} slotKey, ok := kgmV3Slot2Key[header.CryptoSlot] if !ok { @@ -33,16 +31,7 @@ func newKgmCryptoV3(header *Header, body io.Reader) (io.Reader, error) { return c, nil } -func (d *kgmCryptoV3) Read(buf []byte) (int, error) { - n, err := d.rd.Read(buf) - if n > 0 { - d.decrypt(buf[:n], d.offset) - d.offset += n - } - return n, err -} - -func (d *kgmCryptoV3) decrypt(b []byte, offset int) { +func (d *kgmCryptoV3) Decrypt(b []byte, offset int) { for i := 0; i < len(b); i++ { b[i] ^= d.fileBox[(offset+i)%len(d.fileBox)] b[i] ^= b[i] << 4 diff --git a/algo/kwm/kwm.go b/algo/kwm/kwm.go index 836a9b0..f222876 100644 --- a/algo/kwm/kwm.go +++ b/algo/kwm/kwm.go @@ -2,8 +2,9 @@ package kwm import ( "bytes" - "encoding/binary" "errors" + "fmt" + "io" "strconv" "strings" "unicode" @@ -11,95 +12,63 @@ import ( "github.com/unlock-music/cli/algo/common" ) -var ( - magicHeader = []byte{ - 0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D, - 0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65} - ErrKwFileSize = errors.New("kwm invalid file size") - ErrKwMagicHeader = errors.New("kwm magic header not matched") -) - +const magicHeader = "yeelion-kuwo-tme" const keyPreDefined = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk" type Decoder struct { - file []byte + cipher common.StreamDecoder + + rd io.ReadSeeker + offset int - key []byte outputExt string bitrate int - mask []byte - - audio []byte -} - -func (d *Decoder) GetAudioData() []byte { - return d.audio } func (d *Decoder) GetAudioExt() string { return "." + d.outputExt } -func (d *Decoder) GetMeta() common.Meta { - return nil -} - -func NewDecoder(data []byte) common.Decoder { - //todo: Notice the input data will be changed for now - return &Decoder{file: data} +func NewDecoder(rd io.ReadSeeker) common.Decoder { + return &Decoder{rd: rd} } func (d *Decoder) Validate() error { - lenData := len(d.file) - if lenData < 1024 { - return ErrKwFileSize - } - if !bytes.Equal(magicHeader, d.file[:16]) { - return ErrKwMagicHeader - } - - return nil -} - -func generateMask(key []byte) []byte { - keyInt := binary.LittleEndian.Uint64(key) - keyStr := strconv.FormatUint(keyInt, 10) - keyStrTrim := padOrTruncate(keyStr, 32) - mask := make([]byte, 32) - for i := 0; i < 32; i++ { - mask[i] = keyPreDefined[i] ^ keyStrTrim[i] - } - return mask -} - -func (d *Decoder) parseBitrateAndType() { - bitType := string(bytes.TrimRight(d.file[0x30:0x38], string(byte(0)))) - charPos := 0 - for charPos = range bitType { - if !unicode.IsNumber(rune(bitType[charPos])) { - break - } - } - var err error - d.bitrate, err = strconv.Atoi(bitType[:charPos]) + header := make([]byte, 0x400) // kwm header is fixed to 1024 bytes + _, err := io.ReadFull(d.rd, header) if err != nil { - d.bitrate = 0 + return fmt.Errorf("kwm read header: %w", err) } - d.outputExt = strings.ToLower(bitType[charPos:]) + // check magic header, 0x00 - 0x0F + if !bytes.Equal([]byte(magicHeader), header[:len(magicHeader)]) { + return errors.New("kwm magic header not matched") + } + + d.cipher = newKwmCipher(header[0x18:0x20]) // Crypto Key, 0x18 - 0x1F + d.bitrate, d.outputExt = parseBitrateAndType(header[0x30:0x38]) // Bitrate & File Extension, 0x30 - 0x38 + + return nil } -func (d *Decoder) Decode() error { - d.parseBitrateAndType() +func parseBitrateAndType(header []byte) (int, string) { + tmp := strings.TrimRight(string(header), "\x00") + sep := strings.IndexFunc(tmp, func(r rune) bool { + return !unicode.IsDigit(r) + }) - d.mask = generateMask(d.file[0x18:0x20]) + bitrate, _ := strconv.Atoi(tmp[:sep]) // just ignore the error + outputExt := strings.ToLower(tmp[sep:]) + return bitrate, outputExt +} - d.audio = d.file[1024:] - dataLen := len(d.audio) - for i := 0; i < dataLen; i++ { - d.audio[i] ^= d.mask[i&0x1F] //equals: [i % 32] +func (d *Decoder) Read(b []byte) (int, error) { + n, err := d.rd.Read(b) + if n > 0 { + d.cipher.Decrypt(b[:n], d.offset) + d.offset += n } - return nil + return n, err } func padOrTruncate(raw string, length int) string { diff --git a/algo/kwm/kwm_cipher.go b/algo/kwm/kwm_cipher.go new file mode 100644 index 0000000..97b8a77 --- /dev/null +++ b/algo/kwm/kwm_cipher.go @@ -0,0 +1,31 @@ +package kwm + +import ( + "encoding/binary" + "strconv" +) + +type kwmCipher struct { + mask []byte +} + +func newKwmCipher(key []byte) *kwmCipher { + return &kwmCipher{mask: generateMask(key)} +} + +func generateMask(key []byte) []byte { + keyInt := binary.LittleEndian.Uint64(key) + keyStr := strconv.FormatUint(keyInt, 10) + keyStrTrim := padOrTruncate(keyStr, 32) + mask := make([]byte, 32) + for i := 0; i < 32; i++ { + mask[i] = keyPreDefined[i] ^ keyStrTrim[i] + } + return mask +} + +func (c kwmCipher) Decrypt(buf []byte, offset int) { + for i := range buf { + buf[i] ^= c.mask[(offset+i)&0x1F] // equivalent: [i % 32] + } +} diff --git a/algo/ncm/ncm.go b/algo/ncm/ncm.go index 7713c9d..5a1fb5a 100644 --- a/algo/ncm/ncm.go +++ b/algo/ncm/ncm.go @@ -16,27 +16,30 @@ import ( "github.com/unlock-music/cli/internal/utils" ) +const magicHeader = "CTENFDAM" + var ( - magicHeader = []byte{ - 0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D} keyCore = []byte{ 0x68, 0x7a, 0x48, 0x52, 0x41, 0x6d, 0x73, 0x6f, - 0x35, 0x6b, 0x49, 0x6e, 0x62, 0x61, 0x78, 0x57} + 0x35, 0x6b, 0x49, 0x6e, 0x62, 0x61, 0x78, 0x57, + } keyMeta = []byte{ 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, - 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28} + 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, + } ) -func NewDecoder(data []byte) common.Decoder { +func NewDecoder(rd io.ReadSeeker) common.Decoder { return &Decoder{ - file: data, - fileLen: uint32(len(data)), + rd: rd, } } type Decoder struct { - file []byte - fileLen uint32 + rd io.ReadSeeker + offset int + + cipher common.StreamDecoder key []byte box []byte @@ -47,91 +50,128 @@ type Decoder struct { cover []byte audio []byte - - offsetKey uint32 - offsetMeta uint32 - offsetCover uint32 - offsetAudio uint32 } func (d *Decoder) Validate() error { - if !bytes.Equal(magicHeader, d.file[:len(magicHeader)]) { - return errors.New("ncm magic header not match") + if err := d.validateMagicHeader(); err != nil { + return err } - d.offsetKey = 8 + 2 + + if _, err := d.rd.Seek(2, io.SeekCurrent); err != nil { // 2 bytes gap + return fmt.Errorf("ncm seek file: %w", err) + } + + keyData, err := d.readKeyData() + if err != nil { + return err + } + + if err := d.readMetaData(); err != nil { + return fmt.Errorf("read meta date failed: %w", err) + } + + if _, err := d.rd.Seek(5, io.SeekCurrent); err != nil { // 5 bytes gap + return fmt.Errorf("ncm seek gap: %w", err) + } + + if err := d.readCoverData(); err != nil { + return fmt.Errorf("parse ncm cover file failed: %w", err) + } + + if err := d.parseMeta(); err != nil { + return fmt.Errorf("parse meta failed: %w", err) + } + + d.cipher = newNcmCipher(keyData) return nil } -func (d *Decoder) readKeyData() error { - if d.offsetKey == 0 || d.offsetKey+4 > d.fileLen { - return errors.New("invalid cover file offset") +func (d *Decoder) validateMagicHeader() error { + header := make([]byte, len(magicHeader)) // 0x00 - 0x07 + if _, err := d.rd.Read(header); err != nil { + return fmt.Errorf("ncm read magic header: %w", err) + } + + if !bytes.Equal([]byte(magicHeader), header) { + return errors.New("ncm magic header not match") + } + + return nil +} + +func (d *Decoder) readKeyData() ([]byte, error) { + bKeyLen := make([]byte, 4) // + if _, err := io.ReadFull(d.rd, bKeyLen); err != nil { + return nil, fmt.Errorf("ncm read key length: %w", err) } - bKeyLen := d.file[d.offsetKey : d.offsetKey+4] iKeyLen := binary.LittleEndian.Uint32(bKeyLen) - d.offsetMeta = d.offsetKey + 4 + iKeyLen bKeyRaw := make([]byte, iKeyLen) + if _, err := io.ReadFull(d.rd, bKeyRaw); err != nil { + return nil, fmt.Errorf("ncm read key data: %w", err) + } for i := uint32(0); i < iKeyLen; i++ { - bKeyRaw[i] = d.file[i+4+d.offsetKey] ^ 0x64 + bKeyRaw[i] ^= 0x64 } - d.key = utils.PKCS7UnPadding(utils.DecryptAes128Ecb(bKeyRaw, keyCore))[17:] - return nil + return utils.PKCS7UnPadding(utils.DecryptAES128ECB(bKeyRaw, keyCore))[17:], nil } func (d *Decoder) readMetaData() error { - if d.offsetMeta == 0 || d.offsetMeta+4 > d.fileLen { - return errors.New("invalid meta file offset") + bMetaLen := make([]byte, 4) // + if _, err := io.ReadFull(d.rd, bMetaLen); err != nil { + return fmt.Errorf("ncm read key length: %w", err) } - bMetaLen := d.file[d.offsetMeta : d.offsetMeta+4] iMetaLen := binary.LittleEndian.Uint32(bMetaLen) - d.offsetCover = d.offsetMeta + 4 + iMetaLen + if iMetaLen == 0 { - return errors.New("no any meta file found") + return nil // no meta data } - // Why sub 22: Remove "163 key(Don't modify):" - bKeyRaw := make([]byte, iMetaLen-22) - for i := uint32(0); i < iMetaLen-22; i++ { - bKeyRaw[i] = d.file[d.offsetMeta+4+22+i] ^ 0x63 + bMetaRaw := make([]byte, iMetaLen) + if _, err := io.ReadFull(d.rd, bMetaRaw); err != nil { + return fmt.Errorf("ncm read meta data: %w", err) + } + bMetaRaw = bMetaRaw[22:] // skip "163 key(Don't modify):" + for i := 0; i < len(bMetaRaw); i++ { + bMetaRaw[i] ^= 0x63 } - cipherText, err := base64.StdEncoding.DecodeString(string(bKeyRaw)) + cipherText, err := base64.StdEncoding.DecodeString(string(bMetaRaw)) if err != nil { return errors.New("decode ncm meta failed: " + err.Error()) } - metaRaw := utils.PKCS7UnPadding(utils.DecryptAes128Ecb(cipherText, keyMeta)) - sepIdx := bytes.IndexRune(metaRaw, ':') - if sepIdx == -1 { + metaRaw := utils.PKCS7UnPadding(utils.DecryptAES128ECB(cipherText, keyMeta)) + sep := bytes.IndexByte(metaRaw, ':') + if sep == -1 { return errors.New("invalid ncm meta file") } - d.metaType = string(metaRaw[:sepIdx]) - d.metaRaw = metaRaw[sepIdx+1:] + d.metaType = string(metaRaw[:sep]) + d.metaRaw = metaRaw[sep+1:] + return nil } -func (d *Decoder) buildKeyBox() { - box := make([]byte, 256) - for i := 0; i < 256; i++ { - box[i] = byte(i) +func (d *Decoder) readCoverData() error { + bCoverCRC := make([]byte, 4) + if _, err := io.ReadFull(d.rd, bCoverCRC); err != nil { + return fmt.Errorf("ncm read cover crc: %w", err) } - keyLen := len(d.key) - var j byte - for i := 0; i < 256; i++ { - j = box[i] + j + d.key[i%keyLen] - box[i], box[j] = box[j], box[i] + bCoverLen := make([]byte, 4) // + if _, err := io.ReadFull(d.rd, bCoverLen); err != nil { + return fmt.Errorf("ncm read cover length: %w", err) } + iCoverLen := binary.LittleEndian.Uint32(bCoverLen) - d.box = make([]byte, 256) - var _i byte - for i := 0; i < 256; i++ { - _i = byte(i + 1) - si := box[_i] - sj := box[_i+si] - d.box[i] = box[si+sj] + coverBuf := make([]byte, iCoverLen) + if _, err := io.ReadFull(d.rd, coverBuf); err != nil { + return fmt.Errorf("ncm read cover data: %w", err) } + d.cover = coverBuf + + return nil } func (d *Decoder) parseMeta() error { @@ -147,54 +187,13 @@ func (d *Decoder) parseMeta() error { } } -func (d *Decoder) readCoverData() error { - if d.offsetCover == 0 || d.offsetCover+13 > d.fileLen { - return errors.New("invalid cover file offset") +func (d *Decoder) Read(buf []byte) (int, error) { + n, err := d.rd.Read(buf) + if n > 0 { + d.cipher.Decrypt(buf[:n], d.offset) + d.offset += n } - - coverLenStart := d.offsetCover + 5 + 4 - bCoverLen := d.file[coverLenStart : coverLenStart+4] - - iCoverLen := binary.LittleEndian.Uint32(bCoverLen) - d.offsetAudio = coverLenStart + 4 + iCoverLen - if iCoverLen == 0 { - return nil - } - d.cover = d.file[coverLenStart+4 : coverLenStart+4+iCoverLen] - return nil -} - -func (d *Decoder) readAudioData() error { - if d.offsetAudio == 0 || d.offsetAudio > d.fileLen { - return errors.New("invalid audio offset") - } - audioRaw := d.file[d.offsetAudio:] - audioLen := len(audioRaw) - d.audio = make([]byte, audioLen) - for i := uint32(0); i < uint32(audioLen); i++ { - d.audio[i] = d.box[i&0xff] ^ audioRaw[i] - } - return nil -} - -func (d *Decoder) Decode() error { - if err := d.readKeyData(); err != nil { - return fmt.Errorf("read key data failed: %w", err) - } - d.buildKeyBox() - - if err := d.readMetaData(); err != nil { - return fmt.Errorf("read meta date failed: %w", err) - } - if err := d.parseMeta(); err != nil { - return fmt.Errorf("parse meta failed: %w", err) - } - - if err := d.readCoverData(); err != nil { - return fmt.Errorf("parse ncm cover file failed: %w", err) - } - - return d.readAudioData() + return n, err } func (d *Decoder) GetAudioExt() string { @@ -206,10 +205,6 @@ func (d *Decoder) GetAudioExt() string { return "" } -func (d *Decoder) GetAudioData() []byte { - return d.audio -} - func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) { if d.cover != nil { return d.cover, nil diff --git a/algo/ncm/ncm_cipher.go b/algo/ncm/ncm_cipher.go new file mode 100644 index 0000000..96a638f --- /dev/null +++ b/algo/ncm/ncm_cipher.go @@ -0,0 +1,42 @@ +package ncm + +type ncmCipher struct { + key []byte + box []byte +} + +func newNcmCipher(key []byte) *ncmCipher { + return &ncmCipher{ + key: key, + box: buildKeyBox(key), + } +} + +func (c *ncmCipher) Decrypt(buf []byte, offset int) { + for i := 0; i < len(buf); i++ { + buf[i] ^= c.box[(i+offset)&0xff] + } +} + +func buildKeyBox(key []byte) []byte { + box := make([]byte, 256) + for i := 0; i < 256; i++ { + box[i] = byte(i) + } + + var j byte + for i := 0; i < 256; i++ { + j = box[i] + j + key[i%len(key)] + box[i], box[j] = box[j], box[i] + } + + ret := make([]byte, 256) + var _i byte + for i := 0; i < 256; i++ { + _i = byte(i + 1) + si := box[_i] + sj := box[_i+si] + ret[i] = box[si+sj] + } + return ret +} diff --git a/algo/qmc/cipher.go b/algo/qmc/cipher.go deleted file mode 100644 index 150f52f..0000000 --- a/algo/qmc/cipher.go +++ /dev/null @@ -1,5 +0,0 @@ -package qmc - -type streamCipher interface { - Decrypt(buf []byte, offset int) -} diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index 4318946..5cbfe15 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "io" "strconv" "strings" @@ -12,14 +13,14 @@ import ( ) type Decoder struct { - r io.ReadSeeker - fileExt string + raw io.ReadSeeker + audio io.Reader + offset int + audioLen int - audioLen int - decodedKey []byte - cipher streamCipher - offset int + cipher common.StreamDecoder + decodedKey []byte rawMetaExtra1 int rawMetaExtra2 int } @@ -27,78 +28,79 @@ type Decoder struct { // Read implements io.Reader, offer the decrypted audio data. // Validate should call before Read to check if the file is valid. func (d *Decoder) Read(p []byte) (int, error) { - n := len(p) - if d.audioLen <= d.offset { - return 0, io.EOF - } else if d.audioLen-d.offset < n { - n = d.audioLen - d.offset + n, err := d.audio.Read(p) + if n > 0 { + d.cipher.Decrypt(p[:n], d.offset) + d.offset += n } - m, err := d.r.Read(p[:n]) - if m > 0 { - d.cipher.Decrypt(p[:m], d.offset) - d.offset += m - } - return m, err + return n, err } -func NewDecoder(r io.ReadSeeker) (*Decoder, error) { - d := &Decoder{r: r} +func NewDecoder(r io.ReadSeeker) common.Decoder { + return &Decoder{raw: r} +} + +func (d *Decoder) Validate() error { + // search & derive key err := d.searchKey() if err != nil { - return nil, err + return err } + // check cipher type and init decode cipher if len(d.decodedKey) > 300 { d.cipher, err = newRC4Cipher(d.decodedKey) if err != nil { - return nil, err + return err } } else if len(d.decodedKey) != 0 { d.cipher, err = newMapCipher(d.decodedKey) if err != nil { - return nil, err + return err } } else { d.cipher = newStaticCipher() } - _, err = d.r.Seek(0, io.SeekStart) - if err != nil { - return nil, err - } - - return d, nil -} - -func (d *Decoder) Validate() error { - buf := make([]byte, 16) - if _, err := io.ReadFull(d.r, buf); err != nil { - return err - } - _, err := d.r.Seek(0, io.SeekStart) - if err != nil { + // test with first 16 bytes + if err := d.validateDecode(); err != nil { return err } - d.cipher.Decrypt(buf, 0) - fileExt, ok := common.SniffAll(buf) - if !ok { - return errors.New("detect file type failed") + // reset position, limit to audio, prepare for Read + if _, err := d.raw.Seek(0, io.SeekStart); err != nil { + return err } - d.fileExt = fileExt + d.audio = io.LimitReader(d.raw, int64(d.audioLen)) + return nil } -func (d *Decoder) GetFileExt() string { - return d.fileExt +func (d *Decoder) validateDecode() error { + _, err := d.raw.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("qmc seek to start: %w", err) + } + + buf := make([]byte, 16) + if _, err := io.ReadFull(d.raw, buf); err != nil { + return fmt.Errorf("qmc read header: %w", err) + } + + d.cipher.Decrypt(buf, 0) + _, ok := common.SniffAll(buf) + if !ok { + return errors.New("qmc: detect file type failed") + } + return nil } func (d *Decoder) searchKey() error { - fileSizeM4, err := d.r.Seek(-4, io.SeekEnd) + fileSizeM4, err := d.raw.Seek(-4, io.SeekEnd) if err != nil { return err } - buf, err := io.ReadAll(io.LimitReader(d.r, 4)) + buf, err := io.ReadAll(io.LimitReader(d.raw, 4)) if err != nil { return err } @@ -118,13 +120,13 @@ func (d *Decoder) searchKey() error { } func (d *Decoder) readRawKey(rawKeyLen int64) error { - audioLen, err := d.r.Seek(-(4 + rawKeyLen), io.SeekEnd) + audioLen, err := d.raw.Seek(-(4 + rawKeyLen), io.SeekEnd) if err != nil { return err } d.audioLen = int(audioLen) - rawKeyData, err := io.ReadAll(io.LimitReader(d.r, rawKeyLen)) + rawKeyData, err := io.ReadAll(io.LimitReader(d.raw, rawKeyLen)) if err != nil { return err } @@ -142,22 +144,22 @@ func (d *Decoder) readRawKey(rawKeyLen int64) error { func (d *Decoder) readRawMetaQTag() error { // get raw meta data len - if _, err := d.r.Seek(-8, io.SeekEnd); err != nil { + if _, err := d.raw.Seek(-8, io.SeekEnd); err != nil { return err } - buf, err := io.ReadAll(io.LimitReader(d.r, 4)) + buf, err := io.ReadAll(io.LimitReader(d.raw, 4)) if err != nil { return err } rawMetaLen := int64(binary.BigEndian.Uint32(buf)) // read raw meta data - audioLen, err := d.r.Seek(-(8 + rawMetaLen), io.SeekEnd) + audioLen, err := d.raw.Seek(-(8 + rawMetaLen), io.SeekEnd) if err != nil { return err } d.audioLen = int(audioLen) - rawMetaData, err := io.ReadAll(io.LimitReader(d.r, rawMetaLen)) + rawMetaData, err := io.ReadAll(io.LimitReader(d.raw, rawMetaLen)) if err != nil { return err } @@ -206,53 +208,6 @@ func init() { "mflac", "mflac0", //QQ Music New Flac } for _, ext := range supportedExts { - common.RegisterDecoder(ext, false, newCompactDecoder) + common.RegisterDecoder(ext, false, NewDecoder) } } - -type compactDecoder struct { - decoder *Decoder - createErr error - buf *bytes.Buffer -} - -func newCompactDecoder(p []byte) common.Decoder { - r := bytes.NewReader(p) - d, err := NewDecoder(r) - c := compactDecoder{ - decoder: d, - createErr: err, - } - return &c -} - -func (c *compactDecoder) Validate() error { - if c.createErr != nil { - return c.createErr - } - return c.decoder.Validate() -} - -func (c *compactDecoder) Decode() error { - if c.createErr != nil { - return c.createErr - } - c.buf = bytes.NewBuffer(nil) - _, err := io.Copy(c.buf, c.decoder) - return err -} - -func (c *compactDecoder) GetAudioData() []byte { - return c.buf.Bytes() -} - -func (c *compactDecoder) GetAudioExt() string { - if c.createErr != nil { - return "" - } - return c.decoder.GetFileExt() -} - -func (c *compactDecoder) GetMeta() common.Meta { - return nil -} diff --git a/algo/tm/tm.go b/algo/tm/tm.go index a73b448..2331469 100644 --- a/algo/tm/tm.go +++ b/algo/tm/tm.go @@ -3,6 +3,8 @@ package tm import ( "bytes" "errors" + "fmt" + "io" "github.com/unlock-music/cli/algo/common" ) @@ -11,66 +13,38 @@ var replaceHeader = []byte{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70} var magicHeader = []byte{0x51, 0x51, 0x4D, 0x55} //0x15, 0x1D, 0x1A, 0x21 type Decoder struct { - file []byte - audio []byte - headerMatch bool - audioExt string -} - -func (d *Decoder) GetAudioData() []byte { - return d.audio -} - -func (d *Decoder) GetAudioExt() string { - if d.audioExt != "" { - return "." + d.audioExt - } - return "" -} - -func (d *Decoder) GetMeta() common.Meta { - return nil + raw io.ReadSeeker + offset int + audio io.Reader } func (d *Decoder) Validate() error { - if len(d.file) < 8 { - return errors.New("invalid file size") + header := make([]byte, 8) + if _, err := io.ReadFull(d.raw, header); err != nil { + return fmt.Errorf("tm read header: %w", err) } - if !bytes.Equal(magicHeader, d.file[:4]) { - return errors.New("not a valid tm file") + if !bytes.Equal(magicHeader, header[:len(magicHeader)]) { + return errors.New("tm: valid magic header") } - d.headerMatch = true + + d.audio = io.MultiReader(bytes.NewReader(replaceHeader), d.raw) return nil } -func (d *Decoder) Decode() error { - d.audio = d.file - if d.headerMatch { - for i := 0; i < 8; i++ { - d.audio[i] = replaceHeader[i] - } - d.audioExt = "m4a" - } - return nil +func (d *Decoder) Read(buf []byte) (int, error) { + return d.audio.Read(buf) } -//goland:noinspection GoUnusedExportedFunction -func NewDecoder(data []byte) common.Decoder { - return &Decoder{file: data} -} +func NewTmDecoder(rd io.ReadSeeker) common.Decoder { + return &Decoder{raw: rd} -func DecoderFuncWithExt(ext string) common.NewDecoderFunc { - return func(file []byte) common.Decoder { - return &Decoder{file: file, audioExt: ext} - } } func init() { // QQ Music IOS M4a - common.RegisterDecoder("tm2", false, DecoderFuncWithExt("m4a")) - common.RegisterDecoder("tm6", false, DecoderFuncWithExt("m4a")) + common.RegisterDecoder("tm2", false, NewTmDecoder) + common.RegisterDecoder("tm6", false, NewTmDecoder) // QQ Music IOS Mp3 common.RegisterDecoder("tm0", false, common.NewRawDecoder) common.RegisterDecoder("tm3", false, common.NewRawDecoder) - } diff --git a/algo/xm/xm.go b/algo/xm/xm.go index 1268d0c..a67b62b 100644 --- a/algo/xm/xm.go +++ b/algo/xm/xm.go @@ -3,6 +3,8 @@ package xm import ( "bytes" "errors" + "fmt" + "io" "github.com/unlock-music/cli/algo/common" ) @@ -16,22 +18,19 @@ var ( " MP3": "mp3", " A4M": "m4a", } - ErrFileSize = errors.New("xm invalid file size") ErrMagicHeader = errors.New("xm magic header not matched") ) type Decoder struct { - file []byte - headerLen uint32 + rd io.ReadSeeker + offset int + + cipher common.StreamDecoder outputExt string mask byte audio []byte } -func (d *Decoder) GetAudioData() []byte { - return d.audio -} - func (d *Decoder) GetAudioExt() string { if d.outputExt != "" { return "." + d.outputExt @@ -40,59 +39,53 @@ func (d *Decoder) GetAudioExt() string { return "" } -func (d *Decoder) GetMeta() common.Meta { - return nil -} - -func NewDecoder(data []byte) common.Decoder { - return &Decoder{file: data} +func NewDecoder(rd io.ReadSeeker) common.Decoder { + return &Decoder{rd: rd} } func (d *Decoder) Validate() error { - lenData := len(d.file) - if lenData < 16 { - return ErrFileSize + header := make([]byte, 16) // xm header is fixed to 16 bytes + + if _, err := io.ReadFull(d.rd, header); err != nil { + return fmt.Errorf("xm read header: %w", err) } - if !bytes.Equal(magicHeader, d.file[:4]) || - !bytes.Equal(magicHeader2, d.file[8:12]) { + + // 0x00 - 0x03 and 0x08 - 0x0B: magic header + if !bytes.Equal(magicHeader, header[:4]) || !bytes.Equal(magicHeader2, header[8:12]) { return ErrMagicHeader } + // 0x04 - 0x07: Audio File Type var ok bool - d.outputExt, ok = typeMapping[string(d.file[4:8])] + d.outputExt, ok = typeMapping[string(header[4:8])] if !ok { - return errors.New("detect unknown xm file type: " + string(d.file[4:8])) + return fmt.Errorf("xm detect unknown audio type: %s", string(header[4:8])) } - d.headerLen = uint32(d.file[12]) | uint32(d.file[13])<<8 | uint32(d.file[14])<<16 // LittleEndian Unit24 - if d.headerLen+16 > uint32(lenData) { - return ErrFileSize - } + // 0x0C - 0x0E, Encrypt Start At, LittleEndian Unit24 + encStartAt := uint32(header[12]) | uint32(header[13])<<8 | uint32(header[14])<<16 + + // 0x0F, XOR Mask + d.cipher = newXmCipher(header[15], int(encStartAt)) + return nil } -func (d *Decoder) Decode() error { - d.mask = d.file[15] - d.audio = d.file[16:] - dataLen := uint32(len(d.audio)) - for i := d.headerLen; i < dataLen; i++ { - d.audio[i] = ^(d.audio[i] - d.mask) - } - return nil -} - -func DecoderFuncWithExt(ext string) common.NewDecoderFunc { - return func(file []byte) common.Decoder { - return &Decoder{file: file, outputExt: ext} +func (d *Decoder) Read(p []byte) (int, error) { + n, err := d.rd.Read(p) + if n > 0 { + d.cipher.Decrypt(p[:n], d.offset) + d.offset += n } + return n, err } func init() { // Xiami Wav/M4a/Mp3/Flac common.RegisterDecoder("xm", false, NewDecoder) // Xiami Typed Format - common.RegisterDecoder("wav", false, DecoderFuncWithExt("wav")) - common.RegisterDecoder("mp3", false, DecoderFuncWithExt("mp3")) - common.RegisterDecoder("flac", false, DecoderFuncWithExt("flac")) - common.RegisterDecoder("m4a", false, DecoderFuncWithExt("m4a")) + common.RegisterDecoder("wav", false, NewDecoder) + common.RegisterDecoder("mp3", false, NewDecoder) + common.RegisterDecoder("flac", false, NewDecoder) + common.RegisterDecoder("m4a", false, NewDecoder) } diff --git a/algo/xm/xm_cipher.go b/algo/xm/xm_cipher.go new file mode 100644 index 0000000..7ab80d0 --- /dev/null +++ b/algo/xm/xm_cipher.go @@ -0,0 +1,21 @@ +package xm + +type xmCipher struct { + mask byte + encryptStartAt int +} + +func newXmCipher(mask byte, encryptStartAt int) *xmCipher { + return &xmCipher{ + mask: mask, + encryptStartAt: encryptStartAt, + } +} + +func (c *xmCipher) Decrypt(buf []byte, offset int) { + for i := 0; i < len(buf); i++ { + if offset+i >= c.encryptStartAt { + buf[i] ^= c.mask + } + } +} diff --git a/cmd/um/main.go b/cmd/um/main.go index de38e44..f7f8787 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -1,8 +1,10 @@ package main import ( + "bytes" "errors" "fmt" + "io" "os" "path/filepath" "runtime" @@ -153,10 +155,11 @@ func dealDirectory(inputDir string, outputDir string, skipNoop bool, removeSourc } func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFunc, removeSource bool) error { - file, err := os.ReadFile(inputFile) + file, err := os.Open(inputFile) if err != nil { return err } + defer file.Close() var dec common.Decoder for _, decFunc := range allDec { @@ -171,26 +174,32 @@ func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFu if dec == nil { return errors.New("no any decoder can resolve the file") } - if err := dec.Decode(); err != nil { - return errors.New("failed while decoding: " + err.Error()) + + header := bytes.NewBuffer(nil) + _, err = io.CopyN(header, dec, 16) + if err != nil { + return fmt.Errorf("read header failed: %w", err) } - outData := dec.GetAudioData() - outExt := dec.GetAudioExt() - if outExt == "" { - if ext, ok := common.SniffAll(outData); ok { - outExt = ext - } else { - outExt = ".mp3" - } + outExt := ".mp3" + if ext, ok := common.SniffAll(header.Bytes()); ok { + outExt = ext } filenameOnly := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) outPath := filepath.Join(outputDir, filenameOnly+outExt) - err = os.WriteFile(outPath, outData, 0644) + outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } + defer outFile.Close() + + if _, err := io.Copy(outFile, header); err != nil { + return err + } + if _, err := io.Copy(outFile, dec); err != nil { + return err + } // if source file need to be removed if removeSource { diff --git a/internal/utils/crypto.go b/internal/utils/crypto.go index c7ce0f5..0fde1a3 100644 --- a/internal/utils/crypto.go +++ b/internal/utils/crypto.go @@ -8,7 +8,7 @@ func PKCS7UnPadding(encrypt []byte) []byte { return encrypt[:(length - unPadding)] } -func DecryptAes128Ecb(data, key []byte) []byte { +func DecryptAES128ECB(data, key []byte) []byte { cipher, _ := aes.NewCipher(key) decrypted := make([]byte, len(data)) size := 16