Compare commits

...

9 Commits

17 changed files with 219 additions and 47 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
.idea
/dist

View File

@ -8,17 +8,20 @@ import (
)
type Decoder struct {
header header
cipher common.StreamDecoder
rd io.ReadSeeker
rd io.ReadSeeker
cipher common.StreamDecoder
offset int
header header
}
func NewDecoder(rd io.ReadSeeker) common.Decoder {
return &Decoder{rd: rd}
}
// Validate checks if the file is a valid Kugou (.kgm, .vpr, .kgma) file.
// rd will be seeked to the beginning of the encrypted audio.
func (d *Decoder) Validate() (err error) {
if err := d.header.FromFile(d.rd); err != nil {
return err

View File

@ -12,13 +12,14 @@ import (
"unlock-music.dev/cli/algo/common"
)
const magicHeader = "yeelion-kuwo-tme"
const magicHeader1 = "yeelion-kuwo-tme"
const magicHeader2 = "yeelion-kuwo\x00\x00\x00\x00"
const keyPreDefined = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
type Decoder struct {
cipher common.StreamDecoder
rd io.ReadSeeker
rd io.ReadSeeker
cipher common.StreamDecoder
offset int
outputExt string
@ -33,6 +34,8 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
return &Decoder{rd: rd}
}
// Validate checks if the file is a valid Kuwo .kw file.
// rd will be seeked to the beginning of the encrypted audio.
func (d *Decoder) Validate() error {
header := make([]byte, 0x400) // kwm header is fixed to 1024 bytes
_, err := io.ReadFull(d.rd, header)
@ -41,7 +44,9 @@ func (d *Decoder) Validate() error {
}
// check magic header, 0x00 - 0x0F
if !bytes.Equal([]byte(magicHeader), header[:len(magicHeader)]) {
magicHeader := header[:0x10]
if !bytes.Equal([]byte(magicHeader1), magicHeader) &&
!bytes.Equal([]byte(magicHeader2), magicHeader) {
return errors.New("kwm magic header not matched")
}

View File

@ -36,22 +36,19 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
}
type Decoder struct {
rd io.ReadSeeker
rd io.ReadSeeker // rd is the original file reader
offset int
cipher common.StreamDecoder
key []byte
box []byte
metaRaw []byte
metaType string
meta RawMeta
cover []byte
audio []byte
cover []byte
}
// Validate checks if the file is a valid Netease .ncm file.
// rd will be seeked to the beginning of the encrypted audio.
func (d *Decoder) Validate() error {
if err := d.validateMagicHeader(); err != nil {
return err

View File

@ -13,14 +13,15 @@ import (
)
type Decoder struct {
raw io.ReadSeeker
audio io.Reader
offset int
audioLen int
raw io.ReadSeeker // raw is the original file reader
cipher common.StreamDecoder
audio io.Reader // audio is the encrypted audio data
audioLen int // audioLen is the audio data length
offset int // offset is the current audio read position
decodedKey []byte // decodedKey is the decoded key for cipher
cipher common.StreamDecoder
decodedKey []byte
rawMetaExtra1 int
rawMetaExtra2 int
}
@ -100,23 +101,29 @@ func (d *Decoder) searchKey() error {
if err != nil {
return err
}
buf, err := io.ReadAll(io.LimitReader(d.raw, 4))
if err != nil {
suffixBuf := make([]byte, 4)
if _, err := io.ReadFull(d.raw, suffixBuf); err != nil {
return err
}
if string(buf) == "QTag" {
switch string(suffixBuf) {
case "QTag":
return d.readRawMetaQTag()
} else if string(buf) == "STag" {
case "STag":
return errors.New("qmc: file with 'STag' suffix doesn't contains media key")
} else {
size := binary.LittleEndian.Uint32(buf)
default:
size := binary.LittleEndian.Uint32(suffixBuf)
if size <= 0xFFFF && size != 0 { // assume size is key len
return d.readRawKey(int64(size))
}
// try to use default static cipher
d.audioLen = int(fileSizeM4 + 4)
return nil
}
}
func (d *Decoder) readRawKey(rawKeyLen int64) error {

View File

@ -45,11 +45,11 @@ func TestMflac0Decoder_Read(t *testing.T) {
t.Fatal(err)
}
d, err := NewDecoder(bytes.NewReader(raw))
if err != nil {
t.Error(err)
return
d := NewDecoder(bytes.NewReader(raw))
if err := d.Validate(); err != nil {
t.Errorf("validate file error = %v", err)
}
buf := make([]byte, len(target))
if _, err := io.ReadFull(d, buf); err != nil {
t.Errorf("read bytes from decoder error = %v", err)
@ -81,19 +81,12 @@ func TestMflac0Decoder_Validate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
d, err := NewDecoder(bytes.NewReader(raw))
if err != nil {
t.Error(err)
return
}
d := NewDecoder(bytes.NewReader(raw))
if err := d.Validate(); err != nil {
t.Errorf("read bytes from decoder error = %v", err)
return
}
if tt.fileExt != d.GetFileExt() {
t.Errorf("Decrypt() got = %v, want %v", d.GetFileExt(), tt.fileExt)
}
})
}
}

View File

@ -13,9 +13,10 @@ 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 {
raw io.ReadSeeker
raw io.ReadSeeker // raw is the original file reader
offset int
audio io.Reader
audio io.Reader // audio is the decrypted audio data
}
func (d *Decoder) Validate() error {

View File

@ -1,4 +1,4 @@
package xm
package xiami
import (
"bytes"
@ -22,13 +22,11 @@ var (
)
type Decoder struct {
rd io.ReadSeeker
rd io.ReadSeeker // rd is the original file reader
offset int
cipher common.StreamDecoder
outputExt string
mask byte
audio []byte
}
func (d *Decoder) GetAudioExt() string {
@ -43,6 +41,8 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
return &Decoder{rd: rd}
}
// Validate checks if the file is a valid xiami .xm file.
// rd will set to the beginning of the encrypted audio data.
func (d *Decoder) Validate() error {
header := make([]byte, 16) // xm header is fixed to 16 bytes

View File

@ -1,4 +1,4 @@
package xm
package xiami
type xmCipher struct {
mask byte

View File

@ -0,0 +1,34 @@
package ximalaya
import (
_ "embed"
"encoding/binary"
)
const x2mHeaderSize = 1024
var x2mKey = [...]byte{'x', 'm', 'l', 'y'}
var x2mScrambleTable = [x2mHeaderSize]uint16{}
//go:embed x2m_scramble_table.bin
var x2mScrambleTableBytes []byte
func init() {
if len(x2mScrambleTableBytes) != 2*x2mHeaderSize {
panic("invalid x3m scramble table")
}
for i := range x3mScrambleTable {
x3mScrambleTable[i] = binary.LittleEndian.Uint16(x2mScrambleTableBytes[i*2:])
}
}
// decryptX2MHeader decrypts the header of ximalaya .x2m file.
// make sure input src is 1024(x2mHeaderSize) bytes long.
func decryptX2MHeader(src []byte) []byte {
dst := make([]byte, len(src))
for dstIdx := range src {
srcIdx := x2mScrambleTable[dstIdx]
dst[dstIdx] = src[srcIdx] ^ x2mKey[dstIdx%len(x2mKey)]
}
return dst
}

Binary file not shown.

View File

@ -0,0 +1,40 @@
package ximalaya
import (
_ "embed"
"encoding/binary"
)
var x3mKey = [...]byte{
'3', '9', '8', '9', 'd', '1', '1', '1',
'a', 'a', 'd', '5', '6', '1', '3', '9',
'4', '0', 'f', '4', 'f', 'c', '4', '4',
'b', '6', '3', '9', 'b', '2', '9', '2',
}
const x3mHeaderSize = 1024
var x3mScrambleTable = [x3mHeaderSize]uint16{}
//go:embed x3m_scramble_table.bin
var x3mScrambleTableBytes []byte
func init() {
if len(x3mScrambleTableBytes) != 2*x3mHeaderSize {
panic("invalid x3m scramble table")
}
for i := range x3mScrambleTable {
x3mScrambleTable[i] = binary.LittleEndian.Uint16(x3mScrambleTableBytes[i*2:])
}
}
// decryptX3MHeader decrypts the header of ximalaya .x3m file.
// make sure input src is 1024 (x3mHeaderSize) bytes long.
func decryptX3MHeader(src []byte) []byte {
dst := make([]byte, len(src))
for dstIdx := range src {
srcIdx := x3mScrambleTable[dstIdx]
dst[dstIdx] = src[srcIdx] ^ x3mKey[dstIdx%len(x3mKey)]
}
return dst
}

Binary file not shown.

56
algo/ximalaya/ximalaya.go Normal file
View File

@ -0,0 +1,56 @@
package ximalaya
import (
"bytes"
"fmt"
"io"
"unlock-music.dev/cli/algo/common"
)
type Decoder struct {
rd io.ReadSeeker
offset int
audio io.Reader
}
func NewDecoder(rd io.ReadSeeker) common.Decoder {
return &Decoder{rd: rd}
}
func (d *Decoder) Validate() error {
encryptedHeader := make([]byte, x2mHeaderSize)
if _, err := io.ReadFull(d.rd, encryptedHeader); err != nil {
return fmt.Errorf("ximalaya read header: %w", err)
}
{ // try to decode with x2m
header := decryptX2MHeader(encryptedHeader)
if _, ok := common.SniffAll(header); ok {
d.audio = io.MultiReader(bytes.NewReader(header), d.rd)
return nil
}
}
{ // try to decode with x3m
// not read file again, since x2m and x3m have the same header size
header := decryptX3MHeader(encryptedHeader)
if _, ok := common.SniffAll(header); ok {
d.audio = io.MultiReader(bytes.NewReader(header), d.rd)
return nil
}
}
return fmt.Errorf("ximalaya: unknown format")
}
func (d *Decoder) Read(p []byte) (n int, err error) {
return d.audio.Read(p)
}
func init() {
common.RegisterDecoder("x2m", false, NewDecoder)
common.RegisterDecoder("x3m", false, NewDecoder)
common.RegisterDecoder("xm", false, NewDecoder)
}

View File

@ -22,7 +22,8 @@ import (
_ "unlock-music.dev/cli/algo/ncm"
_ "unlock-music.dev/cli/algo/qmc"
_ "unlock-music.dev/cli/algo/tm"
_ "unlock-music.dev/cli/algo/xm"
_ "unlock-music.dev/cli/algo/xiami"
_ "unlock-music.dev/cli/algo/ximalaya"
"unlock-music.dev/cli/internal/logging"
)

View File

@ -7,6 +7,7 @@ import (
func NewZapLogger() (*zap.Logger, error) {
zapCfg := zap.NewDevelopmentConfig()
zapCfg.DisableStacktrace = true
zapCfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
zapCfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006/01/02 15:04:05.000")
return zapCfg.Build()

32
misc/release.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
set -e
PLATFORMS=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
"windows/amd64"
"windows/386"
)
DEST_DIR=${DEST_DIR:-"dist"}
for PLATFORM in "${PLATFORMS[@]}"; do
GOOS=${PLATFORM%/*}
GOARCH=${PLATFORM#*/}
echo "Building for $GOOS/$GOARCH"
FILENAME="um-$GOOS-$GOARCH"
if [ "$GOOS" = "windows" ]; then
FILENAME="$FILENAME.exe"
fi
GOOS=$GOOS GOARCH=$GOARCH go build -v \
-o "${DEST_DIR}/${FILENAME}" \
-ldflags "-s -w -X main.AppVersion=$(git describe --tags --always --dirty)" \
./cmd/um
done
cd "$DEST_DIR"
sha256sum um-* > sha256sums.txt