diff --git a/algo/qmc/key_dec.go b/algo/qmc/key_dec.go new file mode 100644 index 0000000..2ed86f1 --- /dev/null +++ b/algo/qmc/key_dec.go @@ -0,0 +1,111 @@ +package qmc + +import ( + "encoding/base64" + "errors" + "math" + + "golang.org/x/crypto/tea" +) + +func simpleMakeKey(salt byte, length int) []byte { + keyBuf := make([]byte, length) + for i := 0; i < length; i++ { + tmp := math.Tan(float64(salt) + float64(i)*0.1) + keyBuf[i] = byte(math.Abs(tmp) * 100.0) + } + return keyBuf +} +func DecryptKey(rawKey []byte) ([]byte, error) { + rawKeyDec := make([]byte, base64.StdEncoding.DecodedLen(len(rawKey))) + _, err := base64.StdEncoding.Decode(rawKeyDec, rawKey) + if err != nil { + return nil, err + } + + simpleKey := simpleMakeKey(106, 8) + teaKey := make([]byte, 16) + for i := 0; i < 8; i++ { + teaKey[i<<1] = simpleKey[i] + teaKey[i<<1+1] = rawKeyDec[i] + } + + rs, err := decryptTencentTea(rawKeyDec[8:], teaKey) + if err != nil { + return nil, err + } + return append(rawKeyDec[:8], rs...), nil +} +func decryptTencentTea(inBuf []byte, key []byte) ([]byte, error) { + const saltLen = 2 + const zeroLen = 7 + if len(inBuf)%8 != 0 { + return nil, errors.New("inBuf size not a multiple of the block size") + } + if len(inBuf) < 16 { + return nil, errors.New("inBuf size too small") + } + + blk, err := tea.NewCipherWithRounds(key, 32) + if err != nil { + return nil, err + } + + destBuf := make([]byte, 8) + blk.Decrypt(destBuf, inBuf) + padLen := int(destBuf[0] & 0x7) + outLen := len(inBuf) - 1 - padLen - saltLen - zeroLen + if padLen+saltLen != 8 { + return nil, errors.New("invalid pad len") + } + out := make([]byte, outLen) + + ivPrev := make([]byte, 8) + ivCur := inBuf[:8] + + inBufPos := 8 + + destIdx := 1 + padLen + cryptBlock := func() { + ivPrev = ivCur + ivCur = inBuf[inBufPos : inBufPos+8] + + xor8Bytes(destBuf, destBuf, inBuf[inBufPos:inBufPos+8]) + blk.Decrypt(destBuf, destBuf) + + inBufPos += 8 + destIdx = 0 + } + for i := 1; i <= saltLen; { + if destIdx < 8 { + destIdx++ + i++ + } else if destIdx == 8 { + cryptBlock() + } + } + + outPos := 0 + for outPos < outLen { + if destIdx < 8 { + out[outPos] = destBuf[destIdx] ^ ivPrev[destIdx] + destIdx++ + outPos++ + } else if destIdx == 8 { + cryptBlock() + } + } + + for i := 1; i <= zeroLen; i++ { + if destBuf[destIdx] != ivPrev[destIdx] { + return nil, errors.New("zero check failed") + } + } + + return out, nil +} +func xor8Bytes(dst, a, b []byte) { + for i := 0; i < 8; i++ { + dst[i] = a[i] ^ b[i] + } +} diff --git a/algo/qmc/key_dec_test.go b/algo/qmc/key_dec_test.go new file mode 100644 index 0000000..de419b0 --- /dev/null +++ b/algo/qmc/key_dec_test.go @@ -0,0 +1,52 @@ +package qmc + +import ( + "os" + "reflect" + "testing" +) + +func TestSimpleMakeKey(t *testing.T) { + expect := []byte{0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b} + t.Run("106,8", func(t *testing.T) { + if got := simpleMakeKey(106, 8); !reflect.DeepEqual(got, expect) { + t.Errorf("simpleMakeKey() = %v, want %v", got, expect) + } + }) +} + +func TestDecryptKey(t *testing.T) { + rc4Raw, err := os.ReadFile("./testdata/rc4_key_raw.bin") + if err != nil { + t.Error(err) + } + rc4Dec, err := os.ReadFile("./testdata/rc4_key.bin") + if err != nil { + t.Error(err) + } + tests := []struct { + name string + rawKey []byte + want []byte + wantErr bool + }{ + { + "512", + rc4Raw, + rc4Dec, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DecryptKey(tt.rawKey) + if (err != nil) != tt.wantErr { + t.Errorf("DecryptKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DecryptKey() got = %v..., want %v...", string(got[:32]), string(tt.want[:32])) + } + }) + } +} diff --git a/algo/qmc/qmc_512.go b/algo/qmc/qmc_512.go new file mode 100644 index 0000000..e0eff66 --- /dev/null +++ b/algo/qmc/qmc_512.go @@ -0,0 +1,125 @@ +package qmc + +import ( + "encoding/binary" + "errors" + "io" + "strconv" + "strings" +) + +type Mflac0Decoder struct { + r io.ReadSeeker + + audioLen int + decodedKey []byte + rc4 *rc4Cipher + offset int + + rawMetaExtra1 int + rawMetaExtra2 int +} + +func (d *Mflac0Decoder) Read(p []byte) (int, error) { + n := len(p) + if d.audioLen-d.offset <= 0 { + return 0, io.EOF + } else if d.audioLen-d.offset < n { + n = d.audioLen - d.offset + } + m, err := d.r.Read(p[:n]) + if m == 0 { + return 0, err + } + + d.rc4.Process(p[:m], d.offset) + d.offset += m + return m, err + +} + +func NewMflac0Decoder(r io.ReadSeeker) (*Mflac0Decoder, error) { + d := &Mflac0Decoder{r: r} + if err := d.searchKey(); err != nil { + return nil, err + } + + if len(d.decodedKey) > 300 { + var err error + d.rc4, err = NewRC4Cipher(d.decodedKey) + if err != nil { + return nil, err + } + } else { + panic("not implement") //todo: impl + } + + _, err := d.r.Seek(0, io.SeekStart) + if err != nil { + return nil, err + } + return d, nil +} + +func (d *Mflac0Decoder) searchKey() error { + if _, err := d.r.Seek(-4, io.SeekEnd); err != nil { + return err + } + buf, err := io.ReadAll(io.LimitReader(d.r, 4)) + if err != nil { + return err + } + if string(buf) == "QTag" { + if err := d.readRawMetaQTag(); err != nil { + return err + } + } // todo: ... + return nil +} + +func (d *Mflac0Decoder) readRawMetaQTag() error { + // get raw meta data len + if _, err := d.r.Seek(-8, io.SeekEnd); err != nil { + return err + } + buf, err := io.ReadAll(io.LimitReader(d.r, 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) + if err != nil { + return err + } + d.audioLen = int(audioLen) + rawMetaData, err := io.ReadAll(io.LimitReader(d.r, rawMetaLen)) + if err != nil { + return err + } + + items := strings.Split(string(rawMetaData), ",") + if len(items) != 3 { + return errors.New("invalid raw meta data") + } + + { + + d.decodedKey, err = DecryptKey([]byte(items[0])) + if err != nil { + return err + } + } + + d.rawMetaExtra1, err = strconv.Atoi(items[1]) + if err != nil { + return err + } + d.rawMetaExtra2, err = strconv.Atoi(items[2]) + if err != nil { + return err + } + + return nil +} diff --git a/algo/qmc/qmc_512_test.go b/algo/qmc/qmc_512_test.go new file mode 100644 index 0000000..218e13f --- /dev/null +++ b/algo/qmc/qmc_512_test.go @@ -0,0 +1,48 @@ +package qmc + +import ( + "bytes" + "io" + "os" + "reflect" + "testing" +) + +func loadTestDataRC4Mflac0() ([]byte, []byte, error) { + encBody, err := os.ReadFile("./testdata/rc4_raw.bin") + if err != nil { + return nil, nil, err + } + encSuffix, err := os.ReadFile("./testdata/rc4_suffix_mflac0.bin") + if err != nil { + return nil, nil, err + } + + target, err := os.ReadFile("./testdata/rc4_target.bin") + if err != nil { + return nil, nil, err + } + return bytes.Join([][]byte{encBody, encSuffix}, nil), target, nil + +} +func TestMflac0Decoder_Read(t *testing.T) { + raw, target, err := loadTestDataRC4Mflac0() + if err != nil { + t.Fatal(err) + } + + t.Run("mflac0-file", func(t *testing.T) { + d, err := NewMflac0Decoder(bytes.NewReader(raw)) + if err != nil { + t.Error(err) + } + buf := make([]byte, len(target)) + if _, err := io.ReadFull(d, buf); err != nil { + t.Errorf("read bytes from decoder error = %v", err) + return + } + if !reflect.DeepEqual(buf, target) { + t.Errorf("Process() got = %v, want %v", buf[:32], target[:32]) + } + }) +} diff --git a/algo/qmc/rc4.go b/algo/qmc/rc4.go new file mode 100644 index 0000000..f886d17 --- /dev/null +++ b/algo/qmc/rc4.go @@ -0,0 +1,133 @@ +package qmc + +import ( + "errors" +) + +// A rc4Cipher is an instance of RC4 using a particular key. +type rc4Cipher struct { + box []byte + key []byte + hash uint32 + boxTmp []byte +} + +// NewRC4Cipher creates and returns a new rc4Cipher. The key argument should be the +// RC4 key, at least 1 byte and at most 256 bytes. +func NewRC4Cipher(key []byte) (*rc4Cipher, error) { + n := len(key) + if n == 0 { + return nil, errors.New("crypto/rc4: invalid key size") + } + + var c = rc4Cipher{key: key} + c.box = make([]byte, n) + c.boxTmp = make([]byte, n) + + for i := 0; i < n; i++ { + c.box[i] = byte(i) + } + + var j = 0 + for i := 0; i < n; i++ { + j = (j + int(c.box[i]) + int(key[i%n])) % n + c.box[i], c.box[j] = c.box[j], c.box[i] + } + c.getHashBase() + return &c, nil +} + +func (c *rc4Cipher) getHashBase() { + c.hash = 1 + for i := 0; i < len(c.key); i++ { + v := uint32(c.key[i]) + if v == 0 { + continue + } + nextHash := c.hash * v + if nextHash == 0 || nextHash <= c.hash { + break + } + c.hash = nextHash + } +} + +const rc4SegmentSize = 5120 + +func (c *rc4Cipher) Process(src []byte, offset int) { + toProcess := len(src) + processed := 0 + markProcess := func(p int) (finished bool) { + offset += p + toProcess -= p + processed += p + return toProcess == 0 + } + + if offset < 128 { + blockSize := toProcess + if blockSize > 128-offset { + blockSize = 128 - offset + } + c.encFirstSegment(src[:blockSize], offset) + if markProcess(blockSize) { + return + } + } + + if offset%rc4SegmentSize != 0 { + blockSize := toProcess + if blockSize > rc4SegmentSize-offset%rc4SegmentSize { + blockSize = rc4SegmentSize - offset%rc4SegmentSize + } + k := src[processed : processed+blockSize] + c.encASegment(k, offset) + if markProcess(blockSize) { + return + } + } + for toProcess > rc4SegmentSize { + c.encASegment(src[processed:processed+rc4SegmentSize], offset) + markProcess(rc4SegmentSize) + } + + if toProcess > 0 { + c.encASegment(src[processed:], offset) + } +} +func (c *rc4Cipher) encFirstSegment(buf []byte, offset int) { + n := len(c.box) + for i := 0; i < len(buf); i++ { + idx1 := offset + i + segmentID := int(c.key[idx1%n]) + idx2 := int(float64(c.hash) / float64((idx1+1)*segmentID) * 100.0) + buf[i] ^= c.key[idx2%n] + } +} + +func (c *rc4Cipher) encASegment(buf []byte, offset int) { + n := len(c.box) + copy(c.boxTmp, c.box) + + segmentID := (offset / rc4SegmentSize) & 0x1FF + + if n <= segmentID { + return + } + + idx2 := int64(float64(c.hash) / + float64((offset/rc4SegmentSize+1)*int(c.key[segmentID])) * + 100.0) + skipLen := int((idx2 & 0x1FF) + int64(offset%rc4SegmentSize)) + + j, k := 0, 0 + + for i := -skipLen; i < len(buf); i++ { + j = (j + 1) % n + k = (int(c.boxTmp[j]) + k) % n + c.boxTmp[j], c.boxTmp[k] = c.boxTmp[k], c.boxTmp[j] + if i >= 0 { + buf[i] ^= c.boxTmp[int(c.boxTmp[j])+int(c.boxTmp[k])%n] + } + } +} diff --git a/algo/qmc/rc4_test.go b/algo/qmc/rc4_test.go new file mode 100644 index 0000000..780ec8f --- /dev/null +++ b/algo/qmc/rc4_test.go @@ -0,0 +1,72 @@ +package qmc + +import ( + "os" + "reflect" + "testing" +) + +func loadTestData() (*rc4Cipher, []byte, []byte, error) { + key, err := os.ReadFile("./testdata/rc4_key.bin") + if err != nil { + return nil, nil, nil, err + } + raw, err := os.ReadFile("./testdata/rc4_raw.bin") + if err != nil { + return nil, nil, nil, err + } + target, err := os.ReadFile("./testdata/rc4_target.bin") + if err != nil { + return nil, nil, nil, err + } + c, err := NewRC4Cipher(key) + if err != nil { + return nil, nil, nil, err + } + return c, raw, target, nil +} +func Test_rc4Cipher_Process(t *testing.T) { + c, raw, target, err := loadTestData() + if err != nil { + t.Errorf("load testing data failed: %s", err) + } + t.Run("overall", func(t *testing.T) { + c.Process(raw, 0) + if !reflect.DeepEqual(raw, target) { + t.Error("overall") + } + }) + +} + +func Test_rc4Cipher_encFirstSegment(t *testing.T) { + c, raw, target, err := loadTestData() + if err != nil { + t.Errorf("load testing data failed: %s", err) + } + t.Run("first-block(0~128)", func(t *testing.T) { + c.Process(raw[:128], 0) + if !reflect.DeepEqual(raw[:128], target[:128]) { + t.Error("first-block(0~128)") + } + }) +} + +func Test_rc4Cipher_encASegment(t *testing.T) { + c, raw, target, err := loadTestData() + if err != nil { + t.Errorf("load testing data failed: %s", err) + } + t.Run("align-block(128~5120)", func(t *testing.T) { + c.Process(raw[128:5120], 128) + if !reflect.DeepEqual(raw[128:5120], target[128:5120]) { + t.Error("align-block(128~5120)") + } + }) + t.Run("simple-block(5120~10240)", func(t *testing.T) { + c.Process(raw[5120:10240], 5120) + if !reflect.DeepEqual(raw[5120:10240], target[5120:10240]) { + t.Error("align-block(128~5120)") + } + }) +} diff --git a/algo/qmc/testdata/rc4_key.bin b/algo/qmc/testdata/rc4_key.bin new file mode 100644 index 0000000..3bd0914 --- /dev/null +++ b/algo/qmc/testdata/rc4_key.bin @@ -0,0 +1 @@ +dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5 \ No newline at end of file diff --git a/algo/qmc/testdata/rc4_key_raw.bin b/algo/qmc/testdata/rc4_key_raw.bin new file mode 100644 index 0000000..39c8a3b --- /dev/null +++ b/algo/qmc/testdata/rc4_key_raw.bin @@ -0,0 +1 @@ +ZFJ6WDNwNVrjEJZB1o6QjkQV2ZbHSw/2Eb00q1+4z9SVWYyFWO1PcSQrJ5326ubLklmk2ab3AEyIKNUu8DFoAoAc9dpzpTmc+pdkBHjM/bW2jWx+dCyC8vMTHE+DHwaK14UEEGW47ZXMDi7PRCQ2Jpm/oXVdHTIlyrc+bRmKfMith0L2lFQ+nW8CCjV6ao5ydwkZhhNOmRdrCDcUXSJH9PveYwra9/wAmGKWSs9nemuMWKnbjp1PkcxNQexicirVTlLX7PVgRyFyzNyUXgu+R2S4WTmLwjd8UsOyW/dc2mEoYt+vY2lq1X4hFBtcQGOAZDeC+mxrN0EcW8tjS6P4TjOjiOKNMxIfMGSWkSKL3H7z5K7nR1AThW20H2bP/LcpsdaL0uZ/js1wFGpdIfFx9rnLC78itL0WwDleIqp9TBMX/NwakGgIPIbjBwfgyD8d8XKYuLEscIH0ZGdjsadB5XjybgdE3ppfeFEcQiqpnodlTaQRm3KDIF9ATClP0mTl8XlsSojsZ468xseS1Ib2iinx/0SkK3UtJDwp8DH3/+ELisgXd69Bf0pve7wbrQzzMUs9/Ogvvo6ULsIkQfApJ8cSegDYklzGXiLNH7hZYnXDLLSNejD7NvQouULSmGsBbGzhZ5If0NP/6AhSbpzqWLDlabTDgeWWnFeZpBnlK6SMxo+YFFk1Y0XLKsd69+jj \ No newline at end of file diff --git a/algo/qmc/testdata/rc4_raw.bin b/algo/qmc/testdata/rc4_raw.bin new file mode 100644 index 0000000..af0e06e Binary files /dev/null and b/algo/qmc/testdata/rc4_raw.bin differ diff --git a/algo/qmc/testdata/rc4_suffix_mflac0.bin b/algo/qmc/testdata/rc4_suffix_mflac0.bin new file mode 100644 index 0000000..63a168a Binary files /dev/null and b/algo/qmc/testdata/rc4_suffix_mflac0.bin differ diff --git a/algo/qmc/testdata/rc4_target.bin b/algo/qmc/testdata/rc4_target.bin new file mode 100644 index 0000000..0d16010 Binary files /dev/null and b/algo/qmc/testdata/rc4_target.bin differ