diff --git a/algo/qmc/cipher.go b/algo/qmc/cipher.go new file mode 100644 index 0000000..150f52f --- /dev/null +++ b/algo/qmc/cipher.go @@ -0,0 +1,5 @@ +package qmc + +type streamCipher interface { + Decrypt(buf []byte, offset int) +} diff --git a/algo/qmc/cipher_map.go b/algo/qmc/cipher_map.go new file mode 100644 index 0000000..56e0626 --- /dev/null +++ b/algo/qmc/cipher_map.go @@ -0,0 +1,39 @@ +package qmc + +import "errors" + +type mapCipher struct { + key []byte + box []byte + size int +} + +func NewMapCipher(key []byte) (*mapCipher, error) { + if len(key) == 0 { + return nil, errors.New("qmc/cipher_map: invalid key size") + } + c := &mapCipher{key: key, size: len(key)} + c.box = make([]byte, c.size) + return c, nil +} + +func (c *mapCipher) getMask(offset int) byte { + if offset > 0x7FFF { + offset %= 0x7FFF + } + idx := (offset*offset + 71214) % c.size + return c.rotate(c.key[idx], byte(idx)&0x7) +} + +func (c *mapCipher) rotate(value byte, bits byte) byte { + rotate := (bits + 4) % 8 + left := value << rotate + right := value >> rotate + return left | right +} + +func (c *mapCipher) Decrypt(buf []byte, offset int) { + for i := 0; i < len(buf); i++ { + buf[i] ^= c.getMask(offset + i) + } +} diff --git a/algo/qmc/cipher_map_test.go b/algo/qmc/cipher_map_test.go new file mode 100644 index 0000000..f3a4b98 --- /dev/null +++ b/algo/qmc/cipher_map_test.go @@ -0,0 +1,40 @@ +package qmc + +import ( + "os" + "reflect" + "testing" +) + +func loadTestMapCipherData() ([]byte, []byte, []byte, error) { + key, err := os.ReadFile("./testdata/mflac_map_key.bin") + if err != nil { + return nil, nil, nil, err + } + raw, err := os.ReadFile("./testdata/mflac_map_raw.bin") + if err != nil { + return nil, nil, nil, err + } + target, err := os.ReadFile("./testdata/mflac_map_target.bin") + if err != nil { + return nil, nil, nil, err + } + return key, raw, target, nil +} +func Test_mapCipher_Decrypt(t *testing.T) { + key, raw, target, err := loadTestMapCipherData() + if err != nil { + t.Fatalf("load testing data failed: %s", err) + } + t.Run("overall", func(t *testing.T) { + c, err := NewMapCipher(key) + if err != nil { + t.Errorf("init mapCipher failed: %s", err) + return + } + c.Decrypt(raw, 0) + if !reflect.DeepEqual(raw, target) { + t.Error("overall") + } + }) +} diff --git a/algo/qmc/rc4.go b/algo/qmc/cipher_rc4.go similarity index 95% rename from algo/qmc/rc4.go rename to algo/qmc/cipher_rc4.go index f886d17..9b140bf 100644 --- a/algo/qmc/rc4.go +++ b/algo/qmc/cipher_rc4.go @@ -17,7 +17,7 @@ type rc4Cipher struct { func NewRC4Cipher(key []byte) (*rc4Cipher, error) { n := len(key) if n == 0 { - return nil, errors.New("crypto/rc4: invalid key size") + return nil, errors.New("qmc/cipher_rc4: invalid key size") } var c = rc4Cipher{key: key} @@ -54,7 +54,7 @@ func (c *rc4Cipher) getHashBase() { const rc4SegmentSize = 5120 -func (c *rc4Cipher) Process(src []byte, offset int) { +func (c *rc4Cipher) Decrypt(src []byte, offset int) { toProcess := len(src) processed := 0 markProcess := func(p int) (finished bool) { diff --git a/algo/qmc/cipher_rc4_test.go b/algo/qmc/cipher_rc4_test.go new file mode 100644 index 0000000..7a1519d --- /dev/null +++ b/algo/qmc/cipher_rc4_test.go @@ -0,0 +1,90 @@ +package qmc + +import ( + "os" + "reflect" + "testing" +) + +func loadTestRC4CipherData() ([]byte, []byte, []byte, error) { + key, err := os.ReadFile("./testdata/mflac0_rc4_key.bin") + if err != nil { + return nil, nil, nil, err + } + raw, err := os.ReadFile("./testdata/mflac0_rc4_raw.bin") + if err != nil { + return nil, nil, nil, err + } + target, err := os.ReadFile("./testdata/mflac0_rc4_target.bin") + if err != nil { + return nil, nil, nil, err + } + + return key, raw, target, nil +} +func Test_rc4Cipher_Decrypt(t *testing.T) { + key, raw, target, err := loadTestRC4CipherData() + if err != nil { + t.Fatalf("load testing data failed: %s", err) + } + t.Run("overall", func(t *testing.T) { + c, err := NewRC4Cipher(key) + if err != nil { + t.Errorf("init rc4Cipher failed: %s", err) + return + } + c.Decrypt(raw, 0) + if !reflect.DeepEqual(raw, target) { + t.Error("overall") + } + }) + +} + +func Test_rc4Cipher_encFirstSegment(t *testing.T) { + key, raw, target, err := loadTestRC4CipherData() + if err != nil { + t.Fatalf("load testing data failed: %s", err) + } + t.Run("first-block(0~128)", func(t *testing.T) { + c, err := NewRC4Cipher(key) + if err != nil { + t.Errorf("init rc4Cipher failed: %s", err) + return + } + c.Decrypt(raw[:128], 0) + if !reflect.DeepEqual(raw[:128], target[:128]) { + t.Error("first-block(0~128)") + } + }) +} + +func Test_rc4Cipher_encASegment(t *testing.T) { + key, raw, target, err := loadTestRC4CipherData() + if err != nil { + t.Fatalf("load testing data failed: %s", err) + } + + t.Run("align-block(128~5120)", func(t *testing.T) { + c, err := NewRC4Cipher(key) + if err != nil { + t.Errorf("init rc4Cipher failed: %s", err) + return + } + c.Decrypt(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, err := NewRC4Cipher(key) + if err != nil { + t.Errorf("init rc4Cipher failed: %s", err) + return + } + c.Decrypt(raw[5120:10240], 5120) + if !reflect.DeepEqual(raw[5120:10240], target[5120:10240]) { + t.Error("align-block(128~5120)") + } + }) +} diff --git a/algo/qmc/key_dec.go b/algo/qmc/key_dec.go index 2ed86f1..9f7ddf8 100644 --- a/algo/qmc/key_dec.go +++ b/algo/qmc/key_dec.go @@ -18,10 +18,14 @@ func simpleMakeKey(salt byte, length int) []byte { } func DecryptKey(rawKey []byte) ([]byte, error) { rawKeyDec := make([]byte, base64.StdEncoding.DecodedLen(len(rawKey))) - _, err := base64.StdEncoding.Decode(rawKeyDec, rawKey) + n, err := base64.StdEncoding.Decode(rawKeyDec, rawKey) if err != nil { return nil, err } + if n < 16 { + return nil, errors.New("key length is too short") + } + rawKeyDec = rawKeyDec[:n] simpleKey := simpleMakeKey(106, 8) teaKey := make([]byte, 16) diff --git a/algo/qmc/key_dec_test.go b/algo/qmc/key_dec_test.go index de419b0..aa4fc4d 100644 --- a/algo/qmc/key_dec_test.go +++ b/algo/qmc/key_dec_test.go @@ -1,6 +1,7 @@ package qmc import ( + "fmt" "os" "reflect" "testing" @@ -14,38 +15,41 @@ func TestSimpleMakeKey(t *testing.T) { } }) } - -func TestDecryptKey(t *testing.T) { - rc4Raw, err := os.ReadFile("./testdata/rc4_key_raw.bin") +func loadDecryptKeyData(name string) ([]byte, []byte, error) { + keyRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s_key_raw.bin", name)) if err != nil { - t.Error(err) + return nil, nil, err } - rc4Dec, err := os.ReadFile("./testdata/rc4_key.bin") + keyDec, err := os.ReadFile(fmt.Sprintf("./testdata/%s_key.bin", name)) if err != nil { - t.Error(err) + return nil, nil, err } + return keyRaw, keyDec, nil +} +func TestDecryptKey(t *testing.T) { + tests := []struct { - name string - rawKey []byte - want []byte - wantErr bool + name string + filename string + wantErr bool }{ - { - "512", - rc4Raw, - rc4Dec, - false, - }, + {"mflac0_rc4(512)", "mflac0_rc4", false}, + {"mflac_map(256)", "mflac_map", false}, } for _, tt := range tests { + raw, want, err := loadDecryptKeyData(tt.filename) + if err != nil { + t.Fatalf("load test data failed: %s", err) + } t.Run(tt.name, func(t *testing.T) { - got, err := DecryptKey(tt.rawKey) + got, err := DecryptKey(raw) 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])) + if !reflect.DeepEqual(got, want) { + t.Errorf("DecryptKey() got = %v..., want %v...", + string(got[:32]), string(want[:32])) } }) } diff --git a/algo/qmc/qmc_512.go b/algo/qmc/qmc_512.go index e0eff66..fe37c47 100644 --- a/algo/qmc/qmc_512.go +++ b/algo/qmc/qmc_512.go @@ -13,7 +13,7 @@ type Mflac0Decoder struct { audioLen int decodedKey []byte - rc4 *rc4Cipher + cipher streamCipher offset int rawMetaExtra1 int @@ -21,6 +21,14 @@ type Mflac0Decoder struct { } func (d *Mflac0Decoder) Read(p []byte) (int, error) { + if d.cipher != nil { + return d.readRC4(p) + } else { + panic("not impl") + //return d.readPlain(p) + } +} +func (d *Mflac0Decoder) readRC4(p []byte) (int, error) { n := len(p) if d.audioLen-d.offset <= 0 { return 0, io.EOF @@ -32,32 +40,37 @@ func (d *Mflac0Decoder) Read(p []byte) (int, error) { return 0, err } - d.rc4.Process(p[:m], d.offset) + d.cipher.Decrypt(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 { + err := d.searchKey() + if err != nil { return nil, err } - if len(d.decodedKey) > 300 { - var err error - d.rc4, err = NewRC4Cipher(d.decodedKey) + if len(d.decodedKey) == 0 { + return nil, errors.New("invalid decoded key") + } else if len(d.decodedKey) > 300 { + d.cipher, err = NewRC4Cipher(d.decodedKey) if err != nil { return nil, err } } else { - panic("not implement") //todo: impl + d.cipher, err = NewMapCipher(d.decodedKey) + if err != nil { + return nil, err + } } - _, err := d.r.Seek(0, io.SeekStart) + _, err = d.r.Seek(0, io.SeekStart) if err != nil { return nil, err } + return d, nil } @@ -73,7 +86,35 @@ func (d *Mflac0Decoder) searchKey() error { if err := d.readRawMetaQTag(); err != nil { return err } - } // todo: ... + } else { + size := binary.LittleEndian.Uint32(buf) + if size < 0x300 { + return d.readRawKey(int64(size)) + } else { + // todo: try to use fixed key + panic("not impl") + } + } + return nil +} + +func (d *Mflac0Decoder) readRawKey(rawKeyLen int64) error { + audioLen, err := d.r.Seek(-(4 + rawKeyLen), io.SeekEnd) + if err != nil { + return err + } + d.audioLen = int(audioLen) + + rawKeyData, err := io.ReadAll(io.LimitReader(d.r, rawKeyLen)) + if err != nil { + return err + } + + d.decodedKey, err = DecryptKey(rawKeyData) + if err != nil { + return err + } + return nil } @@ -104,12 +145,9 @@ func (d *Mflac0Decoder) readRawMetaQTag() error { return errors.New("invalid raw meta data") } - { - - d.decodedKey, err = DecryptKey([]byte(items[0])) - if err != nil { - return err - } + d.decodedKey, err = DecryptKey([]byte(items[0])) + if err != nil { + return err } d.rawMetaExtra1, err = strconv.Atoi(items[1]) diff --git a/algo/qmc/qmc_512_test.go b/algo/qmc/qmc_512_test.go index 218e13f..b2bfd50 100644 --- a/algo/qmc/qmc_512_test.go +++ b/algo/qmc/qmc_512_test.go @@ -2,23 +2,24 @@ package qmc import ( "bytes" + "fmt" "io" "os" "reflect" "testing" ) -func loadTestDataRC4Mflac0() ([]byte, []byte, error) { - encBody, err := os.ReadFile("./testdata/rc4_raw.bin") +func loadTestDataQmcDecoder(filename string) ([]byte, []byte, error) { + encBody, err := os.ReadFile(fmt.Sprintf("./testdata/%s_raw.bin", filename)) if err != nil { return nil, nil, err } - encSuffix, err := os.ReadFile("./testdata/rc4_suffix_mflac0.bin") + encSuffix, err := os.ReadFile(fmt.Sprintf("./testdata/%s_suffix.bin", filename)) if err != nil { return nil, nil, err } - target, err := os.ReadFile("./testdata/rc4_target.bin") + target, err := os.ReadFile(fmt.Sprintf("./testdata/%s_target.bin", filename)) if err != nil { return nil, nil, err } @@ -26,23 +27,35 @@ func loadTestDataRC4Mflac0() ([]byte, []byte, error) { } func TestMflac0Decoder_Read(t *testing.T) { - raw, target, err := loadTestDataRC4Mflac0() - if err != nil { - t.Fatal(err) + tests := []struct { + name string + filename string + wantErr bool + }{ + {"mflac0_rc4(512)", "mflac0_rc4", false}, + {"mflac_map(256)", "mflac_map", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + raw, target, err := loadTestDataQmcDecoder(tt.filename) + if err != nil { + t.Fatal(err) + } + + 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("Decrypt() got = %v, want %v", buf[:32], target[:32]) + } + }) } - 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_test.go b/algo/qmc/rc4_test.go deleted file mode 100644 index 780ec8f..0000000 --- a/algo/qmc/rc4_test.go +++ /dev/null @@ -1,72 +0,0 @@ -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/mflac0_rc4_key.bin similarity index 100% rename from algo/qmc/testdata/rc4_key.bin rename to algo/qmc/testdata/mflac0_rc4_key.bin diff --git a/algo/qmc/testdata/rc4_key_raw.bin b/algo/qmc/testdata/mflac0_rc4_key_raw.bin similarity index 100% rename from algo/qmc/testdata/rc4_key_raw.bin rename to algo/qmc/testdata/mflac0_rc4_key_raw.bin diff --git a/algo/qmc/testdata/mflac0_rc4_raw.bin b/algo/qmc/testdata/mflac0_rc4_raw.bin new file mode 100644 index 0000000..fd7e4af Binary files /dev/null and b/algo/qmc/testdata/mflac0_rc4_raw.bin differ diff --git a/algo/qmc/testdata/rc4_suffix_mflac0.bin b/algo/qmc/testdata/mflac0_rc4_suffix.bin similarity index 100% rename from algo/qmc/testdata/rc4_suffix_mflac0.bin rename to algo/qmc/testdata/mflac0_rc4_suffix.bin diff --git a/algo/qmc/testdata/mflac0_rc4_target.bin b/algo/qmc/testdata/mflac0_rc4_target.bin new file mode 100644 index 0000000..a7f86c7 Binary files /dev/null and b/algo/qmc/testdata/mflac0_rc4_target.bin differ diff --git a/algo/qmc/testdata/rc4_raw.bin b/algo/qmc/testdata/rc4_raw.bin deleted file mode 100644 index af0e06e..0000000 Binary files a/algo/qmc/testdata/rc4_raw.bin and /dev/null differ diff --git a/algo/qmc/testdata/rc4_target.bin b/algo/qmc/testdata/rc4_target.bin deleted file mode 100644 index 0d16010..0000000 Binary files a/algo/qmc/testdata/rc4_target.bin and /dev/null differ