diff --git a/src/decrypt/qmc.test.ts b/src/decrypt/qmc.test.ts new file mode 100644 index 0000000..5bb2af9 --- /dev/null +++ b/src/decrypt/qmc.test.ts @@ -0,0 +1,32 @@ +import fs from "fs"; +import {QmcDecoder} from "@/decrypt/qmc"; +import {BytesEqual} from "@/decrypt/utils"; + +function loadTestDataDecoder(name: string): { + cipherText: Uint8Array, + clearText: Uint8Array +} { + const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`); + const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`); + const cipherText = new Uint8Array(cipherBody.length + cipherSuffix.length); + cipherText.set(cipherBody); + cipherText.set(cipherSuffix, cipherBody.length); + return { + cipherText, + clearText: fs.readFileSync(`testdata/${name}_target.bin`) + } +} + +test("qmc: real file", async () => { + const cases = ["mflac0_rc4", "mflac_map", "mgg_map", "qmc0_static"] + for (const name of cases) { + const {clearText, cipherText} = loadTestDataDecoder(name) + const c = new QmcDecoder(cipherText) + const buf = c.decrypt() + + expect(BytesEqual(buf, clearText)).toBeTruthy() + } +}) + + + diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index a593cd7..6efa3eb 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,4 +1,4 @@ -import {QmcStaticCipher} from "./qmc_cipher"; +import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher"; import { AudioMimeType, GetArrayBuffer, @@ -10,12 +10,13 @@ import { WriteMetaToMp3 } from "@/decrypt/utils"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; -import {DecryptQMCv2} from "./qmcv2"; +import {DecryptQMCWasm} from "./qmc_wasm"; import iconv from "iconv-lite"; import {DecryptResult} from "@/decrypt/entity"; import {queryAlbumCover} from "@/utils/api"; +import {QmcDeriveKey} from "@/decrypt/qmc_key"; interface Handler { ext: string @@ -54,22 +55,19 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) const fileBuffer = await GetArrayBuffer(file); let musicDecoded: Uint8Array | undefined; - if (version === 2) { - const v2Decrypted = await DecryptQMCv2(fileBuffer); + if (version === 2 && globalThis.WebAssembly) { + console.log("qmc: using wasm decoder") + const v2Decrypted = await DecryptQMCWasm(fileBuffer); // 如果 v2 检测失败,降级到 v1 再尝试一次 if (v2Decrypted) { musicDecoded = v2Decrypted; - } else { - version = 1; } } - - if (version === 1) { - const seed = new QmcStaticCipher(); - musicDecoded = new Uint8Array(fileBuffer) - seed.decrypt(musicDecoded, 0); - } else if (!musicDecoded) { - throw new Error(`解密失败: ${raw_ext}`); + if (!musicDecoded) { + // may throw error + console.log("qmc: using js decoder") + const d = new QmcDecoder(new Uint8Array(fileBuffer)) + musicDecoded = d.decrypt() } const ext = SniffAudioExt(musicDecoded, handler.ext); @@ -137,3 +135,64 @@ async function getCoverImage(title: string, artist?: string, album?: string): Pr } return "" } + +export class QmcDecoder { + file: Uint8Array + size: number + decoded: boolean = false + audioSize?: number + private static readonly BYTE_COMMA = ','.charCodeAt(0) + cipher?: QmcStreamCipher + + constructor(file: Uint8Array) { + this.file = file + this.size = file.length + this.searchKey() + } + + decrypt(): Uint8Array { + if (!this.cipher) { + throw new Error("no cipher found") + } + if (!this.audioSize || this.audioSize <= 0) { + throw new Error("invalid audio size") + } + const audioBuf = this.file.subarray(0, this.audioSize) + + if (!this.decoded) { + this.cipher.decrypt(audioBuf, 0) + this.decoded = true + } + + return audioBuf + } + + private searchKey() { + const last4Byte = this.file.slice(-4); + const textEnc = new TextDecoder() + if (textEnc.decode(last4Byte) === 'QTag') { + const sizeBuf = this.file.slice(-8, -4) + const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset) + const keySize = sizeView.getUint32(0, false) + this.audioSize = this.size - keySize - 8 + const rawKey = this.file.subarray(this.audioSize, this.size - 8) + const keyEnd = rawKey.findIndex(v => v == QmcDecoder.BYTE_COMMA) + const keyDec = QmcDeriveKey(rawKey.subarray(0, keyEnd)) + this.cipher = new QmcRC4Cipher(keyDec) + } else { + const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset); + const keySize = sizeView.getUint32(0, true) + if (keySize < 0x300) { + this.audioSize = this.size - keySize - 4 + const rawKey = this.file.subarray(this.audioSize, this.size - 4) + const keyDec = QmcDeriveKey(rawKey) + this.cipher = new QmcMapCipher(keyDec) + } else { + this.audioSize = this.size + this.cipher = new QmcStaticCipher() + } + } + } + + +} diff --git a/src/decrypt/qmc_cipher.test.ts b/src/decrypt/qmc_cipher.test.ts index 1e40c60..a54d0f8 100644 --- a/src/decrypt/qmc_cipher.test.ts +++ b/src/decrypt/qmc_cipher.test.ts @@ -1,4 +1,5 @@ -import {QmcStaticCipher} from "@/decrypt/qmc_cipher"; +import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher"; +import fs from 'fs' test("static cipher [0x7ff8,0x8000) ", () => { const expected = new Uint8Array([ @@ -25,3 +26,90 @@ test("static cipher [0,0x10) ", () => { expect(buf).toStrictEqual(expected) }) + + +test("map cipher: get mask", () => { + const expected = new Uint8Array([ + 0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB, + 0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79, + ]) + const key = new Uint8Array(256) + for (let i = 0; i < 256; i++) key[i] = i + const buf = new Uint8Array(16) + + const c = new QmcMapCipher(key) + c.decrypt(buf, 0) + expect(buf).toStrictEqual(expected) +}) + +function loadTestDataCipher(name: string): { + key: Uint8Array, + cipherText: Uint8Array, + clearText: Uint8Array +} { + return { + key: fs.readFileSync(`testdata/${name}_key.bin`), + cipherText: fs.readFileSync(`testdata/${name}_raw.bin`), + clearText: fs.readFileSync(`testdata/${name}_target.bin`) + } +} + +test("map cipher: real file", async () => { + const cases = ["mflac_map", "mgg_map"] + for (const name of cases) { + const {key, clearText, cipherText} = loadTestDataCipher(name) + const c = new QmcMapCipher(key) + + c.decrypt(cipherText, 0) + + expect(cipherText).toStrictEqual(clearText) + } +}) + +test("rc4 cipher: real file", async () => { + const cases = ["mflac0_rc4"] + for (const name of cases) { + const {key, clearText, cipherText} = loadTestDataCipher(name) + const c = new QmcRC4Cipher(key) + + c.decrypt(cipherText, 0) + + expect(cipherText).toStrictEqual(clearText) + } +}) + +test("rc4 cipher: first segment", async () => { + const cases = ["mflac0_rc4"] + for (const name of cases) { + const {key, clearText, cipherText} = loadTestDataCipher(name) + const c = new QmcRC4Cipher(key) + + const buf = cipherText.slice(0, 128) + c.decrypt(buf, 0) + expect(buf).toStrictEqual(clearText.slice(0, 128)) + } +}) + +test("rc4 cipher: align block (128~5120)", async () => { + const cases = ["mflac0_rc4"] + for (const name of cases) { + const {key, clearText, cipherText} = loadTestDataCipher(name) + const c = new QmcRC4Cipher(key) + + const buf = cipherText.slice(128, 5120) + c.decrypt(buf, 128) + expect(buf).toStrictEqual(clearText.slice(128, 5120)) + } +}) + +test("rc4 cipher: simple block (5120~10240)", async () => { + const cases = ["mflac0_rc4"] + for (const name of cases) { + const {key, clearText, cipherText} = loadTestDataCipher(name) + const c = new QmcRC4Cipher(key) + + const buf = cipherText.slice(5120, 10240) + c.decrypt(buf, 5120) + expect(buf).toStrictEqual(clearText.slice(5120, 10240)) + } +}) diff --git a/src/decrypt/qmc_cipher.ts b/src/decrypt/qmc_cipher.ts index eef68a7..a6bc53e 100644 --- a/src/decrypt/qmc_cipher.ts +++ b/src/decrypt/qmc_cipher.ts @@ -1,47 +1,47 @@ -const staticCipherBox = new Uint8Array([ - 0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00 - 0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08 - 0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10 - 0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18 - 0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20 - 0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28 - 0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30 - 0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38 - 0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40 - 0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48 - 0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50 - 0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58 - 0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60 - 0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68 - 0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70 - 0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78 - 0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80 - 0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88 - 0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90 - 0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98 - 0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0 - 0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8 - 0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0 - 0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8 - 0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0 - 0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8 - 0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0 - 0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8 - 0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0 - 0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8 - 0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0 - 0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8 -]) - -interface streamCipher { +export interface QmcStreamCipher { decrypt(buf: Uint8Array, offset: number): void } -export class QmcStaticCipher implements streamCipher { + +export class QmcStaticCipher implements QmcStreamCipher { + private static readonly staticCipherBox: Uint8Array = new Uint8Array([ + 0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00 + 0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08 + 0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10 + 0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18 + 0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20 + 0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28 + 0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30 + 0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38 + 0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40 + 0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48 + 0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50 + 0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58 + 0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60 + 0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68 + 0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70 + 0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78 + 0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80 + 0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88 + 0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90 + 0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98 + 0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0 + 0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8 + 0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0 + 0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8 + 0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0 + 0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8 + 0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0 + 0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8 + 0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0 + 0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8 + 0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0 + 0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8 + ]) public getMask(offset: number) { if (offset > 0x7FFF) offset %= 0x7FFF - return staticCipherBox[(offset * offset + 27) & 0xff] + return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff] } public decrypt(buf: Uint8Array, offset: number) { @@ -51,3 +51,152 @@ export class QmcStaticCipher implements streamCipher { } } +export class QmcMapCipher implements QmcStreamCipher { + key: Uint8Array + n: number + + constructor(key: Uint8Array) { + if (key.length == 0) throw Error("qmc/cipher_map: invalid key size") + + this.key = key + this.n = key.length + } + + private static rotate(value: number, bits: number) { + let rotate = (bits + 4) % 8; + let left = value << rotate; + let right = value >> rotate; + return (left | right) & 0xff; + } + + decrypt(buf: Uint8Array, offset: number): void { + for (let i = 0; i < buf.length; i++) { + buf[i] ^= this.getMask(offset + i) + } + } + + private getMask(offset: number) { + if (offset > 0x7fff) offset %= 0x7fff; + + const idx = (offset * offset + 71214) % this.n; + return QmcMapCipher.rotate(this.key[idx], idx & 0x7) + } + +} + +export class QmcRC4Cipher implements QmcStreamCipher { + private static readonly FIRST_SEGMENT_SIZE = 0x80; + private static readonly SEGMENT_SIZE = 5120 + + S: Uint8Array + N: number + key: Uint8Array + hash: number + + constructor(key: Uint8Array) { + if (key.length == 0) { + throw Error("invalid key size") + } + + this.key = key + this.N = key.length + + // init seed box + this.S = new Uint8Array(this.N); + for (let i = 0; i < this.N; ++i) { + this.S[i] = i & 0xff; + } + let j = 0; + for (let i = 0; i < this.N; ++i) { + j = (this.S[i] + j + this.key[i % this.N]) % this.N; + [this.S[i], this.S[j]] = [this.S[j], this.S[i]] + } + + // init hash base + this.hash = 1; + for (let i = 0; i < this.N; i++) { + let value = this.key[i]; + + // ignore if key char is '\x00' + if (!value) continue; + + const next_hash = (this.hash * value) & 0xffffffff; + if (next_hash == 0 || next_hash <= this.hash) break; + + this.hash = next_hash; + } + + } + + decrypt(buf: Uint8Array, offset: number): void { + let toProcess = buf.length; + let processed = 0; + const postProcess = (len: number): boolean => { + toProcess -= len; + processed += len + offset += len + return toProcess == 0 + } + + // Initial segment + if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) { + const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset); + this.encFirstSegment(buf.subarray(0, len_segment), offset); + if (postProcess(len_segment)) return + } + + // align segment + if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) { + const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess); + this.encASegment(buf.subarray(processed, processed + len_segment), offset); + if (postProcess(len_segment)) return + } + + // Batch process segments + while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) { + this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset); + postProcess(QmcRC4Cipher.SEGMENT_SIZE) + } + + // Last segment (incomplete segment) + if (toProcess > 0) { + this.encASegment(buf.subarray(processed), offset); + } + + } + + private encFirstSegment(buf: Uint8Array, offset: number) { + for (let i = 0; i < buf.length; i++) { + + buf[i] ^= this.key[this.getSegmentKey(offset + i)]; + } + } + + private encASegment(buf: Uint8Array, offset: number) { + // Initialise a new seed box + const S = this.S.slice(0) + + // Calculate the number of bytes to skip. + // The initial "key" derived from segment id, plus the current offset. + const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE) + + // decrypt the block + let j = 0; + let k = 0; + for (let i = -skipLen; i < buf.length; i++) { + j = (j + 1) % this.N; + k = (S[j] + k) % this.N; + [S[k], S[j]] = [S[j], S[k]] + + if (i >= 0) { + buf[i] ^= S[(S[j] + S[k]) % this.N]; + } + } + } + + private getSegmentKey(id: number): number { + const seed = this.key[id % this.N] + const idx = (this.hash / ((id + 1) * seed) * 100.0) | 0; + return idx % this.N + } +} diff --git a/src/decrypt/qmc_key.test.ts b/src/decrypt/qmc_key.test.ts new file mode 100644 index 0000000..74e37de --- /dev/null +++ b/src/decrypt/qmc_key.test.ts @@ -0,0 +1,30 @@ +import {QmcDeriveKey, simpleMakeKey} from "@/decrypt/qmc_key"; +import fs from "fs"; + +test("key dec: make simple key", () => { + expect( + simpleMakeKey(106, 8) + ).toStrictEqual( + [0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b] + ) +}) + +function loadTestDataKeyDecrypt(name: string): { + cipherText: Uint8Array, + clearText: Uint8Array +} { + return { + cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`), + clearText: fs.readFileSync(`testdata/${name}_key.bin`) + } +} + +test("key dec: real file", async () => { + const cases = ["mflac_map", "mgg_map", "mflac0_rc4"] + for (const name of cases) { + const {clearText, cipherText} = loadTestDataKeyDecrypt(name) + const buf = QmcDeriveKey(cipherText) + + expect(buf).toStrictEqual(clearText) + } +}) diff --git a/src/decrypt/qmc_key.ts b/src/decrypt/qmc_key.ts new file mode 100644 index 0000000..5914b34 --- /dev/null +++ b/src/decrypt/qmc_key.ts @@ -0,0 +1,107 @@ +import {TeaCipher} from "@/utils/tea"; + +const SALT_LEN = 2 +const ZERO_LEN = 7 + +export function QmcDeriveKey(raw: Uint8Array): Uint8Array { + const textDec = new TextDecoder() + const rawDec = Buffer.from(textDec.decode(raw), 'base64') + let n = rawDec.length; + if (n < 16) { + throw Error("key length is too short") + } + + const simpleKey = simpleMakeKey(106, 8) + let teaKey = new Uint8Array(16); + for (let i = 0; i < 8; i++) { + teaKey[i << 1] = simpleKey[i]; + teaKey[(i << 1) + 1] = rawDec[i]; + } + const sub = decryptTencentTea(rawDec.subarray(8), teaKey) + rawDec.set(sub, 8) + return rawDec.subarray(0, 8 + sub.length) + +} + +// simpleMakeKey exported only for unit test +export function simpleMakeKey(salt: number, length: number): number[] { + const keyBuf: number[] = [] + for (let i = 0; i < length; i++) { + const tmp = Math.tan(salt + i * 0.1) + keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0) + } + return keyBuf +} + + +function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array { + if (inBuf.length % 8 != 0) { + throw Error("inBuf size not a multiple of the block size") + } + if (inBuf.length < 16) { + throw Error("inBuf size too small") + } + + const blk = new TeaCipher(key, 32) + + const tmpBuf = new Uint8Array(8); + const tmpView = new DataView(tmpBuf.buffer); + + blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8)) + + const nPadLen = tmpBuf[0] & 0x7;//只要最低三位 + /*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/ + const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN; + const outBuf = new Uint8Array(outLen) + + let ivPrev = new Uint8Array(8); + let ivCur = inBuf.slice(0, 8); // init iv + let inBufPos = 8; + + + // 跳过 Padding Len 和 Padding + let tmpIdx = 1 + nPadLen; + + // CBC IV 处理 + const cryptBlock = () => { + ivPrev = ivCur; + ivCur = inBuf.slice(inBufPos, inBufPos + 8) + for (let j = 0; j < 8; j++) { + tmpBuf[j] ^= ivCur[j] + } + blk.decrypt(tmpView, tmpView) + inBufPos += 8; + tmpIdx = 0; + } + + // 跳过 Salt + for (let i = 1; i <= SALT_LEN;) { + if (tmpIdx < 8) { + tmpIdx++; + i++; + } else { + cryptBlock() + } + } + + // 还原明文 + let outBufPos = 0; + while (outBufPos < outLen) { + if (tmpIdx < 8) { + outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx]; + outBufPos++ + tmpIdx++; + } else { + cryptBlock() + } + } + + // 校验Zero + for (let i = 1; i <= ZERO_LEN; i++) { + if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) { + throw Error("zero check failed") + } + } + return outBuf +} + diff --git a/src/decrypt/qmcv2.ts b/src/decrypt/qmc_wasm.ts similarity index 97% rename from src/decrypt/qmcv2.ts rename to src/decrypt/qmc_wasm.ts index 3189426..7a1fd49 100644 --- a/src/decrypt/qmcv2.ts +++ b/src/decrypt/qmc_wasm.ts @@ -29,7 +29,7 @@ function MergeUint8Array(array: Uint8Array[]): Uint8Array { * @param {ArrayBuffer} mggBlob 读入的文件 Blob * @return {Promise} */ -export async function DecryptQMCv2(mggBlob: ArrayBuffer) { +export async function DecryptQMCWasm(mggBlob: ArrayBuffer) { // 初始化模组 const QMCCrypto = await QMCCryptoModule(); diff --git a/src/decrypt/utils.ts b/src/decrypt/utils.ts index cef54f6..04b2125 100644 --- a/src/decrypt/utils.ts +++ b/src/decrypt/utils.ts @@ -32,13 +32,20 @@ export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean { }) } +export function BytesEqual(a: Uint8Array, b: Uint8Array,): boolean { + if (a.length !== b.length) return false + return a.every((val, idx) => { + return val === b[idx]; + }) +} + export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string { if (BytesHasPrefix(data, MP3_HEADER)) return "mp3" if (BytesHasPrefix(data, FLAC_HEADER)) return "flac" if (BytesHasPrefix(data, OGG_HEADER)) return "ogg" if (data.length >= 4 + M4A_HEADER.length && - BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a" + BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a" if (BytesHasPrefix(data, WAV_HEADER)) return "wav" if (BytesHasPrefix(data, WMA_HEADER)) return "wma" if (BytesHasPrefix(data, AAC_HEADER)) return "aac" diff --git a/src/utils/tea.test.ts b/src/utils/tea.test.ts new file mode 100644 index 0000000..a3f9273 --- /dev/null +++ b/src/utils/tea.test.ts @@ -0,0 +1,77 @@ +// Copyright 2021 MengYX. All rights reserved. +// +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in https://go.dev/LICENSE. + +import {TeaCipher} from "@/utils/tea"; + + +test("key size", () => { + const testKey = new Uint8Array([ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, + 0x00 + ]) + expect(() => new TeaCipher(testKey.slice(0, 16))) + .not.toThrow() + + expect(() => new TeaCipher(testKey)) + .toThrow() + + expect(() => new TeaCipher(testKey.slice(0, 15))) + .toThrow() + +}) + + +const teaTests = [ + // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec + { + rounds: TeaCipher.numRounds, + key: new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]), + }, + { + rounds: TeaCipher.numRounds, + key: new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]), + }, + { + rounds: 16, + key: new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]), + }, +] + +test("rounds", () => { + const tt = teaTests[0]; + expect(() => new TeaCipher(tt.key, tt.rounds - 1)) + .toThrow() +}) + + +test("encrypt & decrypt", () => { + for (const tt of teaTests) { + const c = new TeaCipher(tt.key, tt.rounds) + + const buf = new Uint8Array(8) + const bufView = new DataView(buf.buffer) + + c.encrypt(bufView, new DataView(tt.plainText.buffer)) + expect(buf).toStrictEqual(tt.cipherText) + + c.decrypt(bufView, new DataView(tt.cipherText.buffer)) + expect(buf).toStrictEqual(tt.plainText) + } +}) + diff --git a/src/utils/tea.ts b/src/utils/tea.ts new file mode 100644 index 0000000..9b4f57a --- /dev/null +++ b/src/utils/tea.ts @@ -0,0 +1,82 @@ +// Copyright 2021 MengYX. All rights reserved. +// +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in https://go.dev/LICENSE. + +// TeaCipher is a typescript port to golang.org/x/crypto/tea + +// Package tea implements the TEA algorithm, as defined in Needham and +// Wheeler's 1994 technical report, “TEA, a Tiny Encryption Algorithm”. See +// http://www.cix.co.uk/~klockstone/tea.pdf for details. +// +// TEA is a legacy cipher and its short block size makes it vulnerable to +// birthday bound attacks (see https://sweet32.info). It should only be used +// where compatibility with legacy systems, not security, is the goal. + +export class TeaCipher { + // BlockSize is the size of a TEA block, in bytes. + static readonly BlockSize = 8; + + // KeySize is the size of a TEA key, in bytes. + static readonly KeySize = 16; + + // delta is the TEA key schedule constant. + static readonly delta = 0x9e3779b9; + + // numRounds 64 is the standard number of rounds in TEA. + static readonly numRounds = 64; + + k0: number + k1: number + k2: number + k3: number + rounds: number + + constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) { + if (key.length != 16) { + throw Error("incorrect key size") + } + if ((rounds & 1) != 0) { + throw Error("odd number of rounds specified") + } + + const k = new DataView(key.buffer) + this.k0 = k.getUint32(0, false) + this.k1 = k.getUint32(4, false) + this.k2 = k.getUint32(8, false) + this.k3 = k.getUint32(12, false) + this.rounds = rounds + } + + + encrypt(dst: DataView, src: DataView) { + + let v0 = src.getUint32(0, false) + let v1 = src.getUint32(4, false) + + let sum = 0 + for (let i = 0; i < this.rounds / 2; i++) { + sum = sum + TeaCipher.delta + v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1) + v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3) + } + + dst.setUint32(0, v0, false) + dst.setUint32(4, v1, false) + } + + decrypt(dst: DataView, src: DataView) { + let v0 = src.getUint32(0, false) + let v1 = src.getUint32(4, false) + + let sum = TeaCipher.delta * this.rounds / 2 + for (let i = 0; i < this.rounds / 2; i++) { + v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3) + v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1) + sum -= TeaCipher.delta + } + dst.setUint32(0, v0, false) + dst.setUint32(4, v1, false) + } +} diff --git a/testdata/mflac0_rc4_key.bin b/testdata/mflac0_rc4_key.bin new file mode 100644 index 0000000..3bd0914 --- /dev/null +++ b/testdata/mflac0_rc4_key.bin @@ -0,0 +1 @@ +dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5 \ No newline at end of file diff --git a/testdata/mflac0_rc4_key_raw.bin b/testdata/mflac0_rc4_key_raw.bin new file mode 100644 index 0000000..39c8a3b --- /dev/null +++ b/testdata/mflac0_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/testdata/mflac0_rc4_raw.bin b/testdata/mflac0_rc4_raw.bin new file mode 100644 index 0000000..fd7e4af Binary files /dev/null and b/testdata/mflac0_rc4_raw.bin differ diff --git a/testdata/mflac0_rc4_suffix.bin b/testdata/mflac0_rc4_suffix.bin new file mode 100644 index 0000000..63a168a Binary files /dev/null and b/testdata/mflac0_rc4_suffix.bin differ diff --git a/testdata/mflac0_rc4_target.bin b/testdata/mflac0_rc4_target.bin new file mode 100644 index 0000000..a7f86c7 Binary files /dev/null and b/testdata/mflac0_rc4_target.bin differ diff --git a/testdata/mflac_map_key.bin b/testdata/mflac_map_key.bin new file mode 100644 index 0000000..7fd7a72 --- /dev/null +++ b/testdata/mflac_map_key.bin @@ -0,0 +1 @@ +yw7xWOyNQ8585Jwx3hjB49QLPKi38F89awnrQ0fq66NT9TDq1ppHNrFqhaDrU5AFk916D58I53h86304GqOFCCyFzBem68DqiXJ81bILEQwG3P3MOnoNzM820kNW9Lv9IJGNn9Xo497p82BLTm4hAX8JLBs0T2pilKvT429sK9jfg508GSk4d047Jxdz5Fou4aa33OkyFRBU3x430mgNBn04Lc9BzXUI2IGYXv3FGa9qE4Vb54kSjVv8ogbg47j3 \ No newline at end of file diff --git a/testdata/mflac_map_key_raw.bin b/testdata/mflac_map_key_raw.bin new file mode 100644 index 0000000..f088613 --- /dev/null +++ b/testdata/mflac_map_key_raw.bin @@ -0,0 +1 @@ +eXc3eFdPeU6+3f7GVeF35bMpIEIQj5JWOWt7G+jsR68Hx3BUFBavkTQ8dpPdP0XBIwPe+OfdsnTGVQqPyg3GCtQSrkgA0mwSQdr4DPzKLkEZFX+Cf1V6ChyipOuC6KT37eAxWMdV1UHf9/OCvydr1dc6SWK1ijRUcP6IAHQhiB+mZLay7XXrSPo32WjdBkn9c9sa2SLtI48atj5kfZ4oOq6QGeld2JA3Z+3wwCe6uTHthKaEHY8ufDYodEe3qqrjYpzkdx55pCtxCQa1JiNqFmJigWm4m3CDzhuJ7YqnjbD+mXxLi7BP1+z4L6nccE2h+DGHVqpGjR9+4LBpe4WHB4DrAzVp2qQRRQJxeHd1v88= \ No newline at end of file diff --git a/testdata/mflac_map_raw.bin b/testdata/mflac_map_raw.bin new file mode 100644 index 0000000..18f70e5 Binary files /dev/null and b/testdata/mflac_map_raw.bin differ diff --git a/testdata/mflac_map_suffix.bin b/testdata/mflac_map_suffix.bin new file mode 100644 index 0000000..f83e4d9 Binary files /dev/null and b/testdata/mflac_map_suffix.bin differ diff --git a/testdata/mflac_map_target.bin b/testdata/mflac_map_target.bin new file mode 100644 index 0000000..7919057 Binary files /dev/null and b/testdata/mflac_map_target.bin differ diff --git a/testdata/mgg_map_key.bin b/testdata/mgg_map_key.bin new file mode 100644 index 0000000..fecf089 --- /dev/null +++ b/testdata/mgg_map_key.bin @@ -0,0 +1 @@ +zGxNk54pKJ0hDkAo80wHE80ycSWQ7z4m4E846zVy2sqCn14F42Y5S7GqeR11WpOV75sDLbE5dFP992t88l0pHy1yAQ49YK6YX6c543drBYLo55Hc4Y0Fyic6LQPiGqu2bG31r8vaq9wS9v63kg0X5VbnOD6RhO4t0RRhk3ajrA7p0iIy027z0L70LZjtw6E18H0D41nz6ASTx71otdF9z1QNC0JmCl51xvnb39zPExEXyKkV47S6QsK5hFh884QJ \ No newline at end of file diff --git a/testdata/mgg_map_key_raw.bin b/testdata/mgg_map_key_raw.bin new file mode 100644 index 0000000..bea6675 --- /dev/null +++ b/testdata/mgg_map_key_raw.bin @@ -0,0 +1 @@ +ekd4Tms1NHC53JEDO/AKVyF+I0bj0hHB7CZeoLDGSApaQB9Oo/pJTBGA/RO+nk5RXLXdHsffLiY4e8kt3LNo6qMl7S89vkiSFxx4Uoq4bGDJ7Jc+bYL6lLsa3M4sBvXS4XcPChrMDz+LmrJMGG6ua2fYyIz1d6TCRUBf1JJgCIkBbDAEeMVYc13qApitiz/apGAPmAnveCaDhfD5GxWsF+RfQ2OcnvrnIXe80Feh/0jx763DlsOBI3eIede6t5zYHokWkZmVEF1jMrnlvsgbQK2EzUWMblmLMsTKNILyZazEoKUyulqmyLO/c/KYE+USPOXPcbjlYFmLhSGHK7sQB5aBR153Yp+xh61ooh2NGAA= \ No newline at end of file diff --git a/testdata/mgg_map_raw.bin b/testdata/mgg_map_raw.bin new file mode 100644 index 0000000..fa8704d Binary files /dev/null and b/testdata/mgg_map_raw.bin differ diff --git a/testdata/mgg_map_suffix.bin b/testdata/mgg_map_suffix.bin new file mode 100644 index 0000000..671d61e Binary files /dev/null and b/testdata/mgg_map_suffix.bin differ diff --git a/testdata/mgg_map_target.bin b/testdata/mgg_map_target.bin new file mode 100644 index 0000000..f318eb9 Binary files /dev/null and b/testdata/mgg_map_target.bin differ diff --git a/testdata/qmc0_static_raw.bin b/testdata/qmc0_static_raw.bin new file mode 100644 index 0000000..80f65f9 Binary files /dev/null and b/testdata/qmc0_static_raw.bin differ diff --git a/testdata/qmc0_static_suffix.bin b/testdata/qmc0_static_suffix.bin new file mode 100644 index 0000000..e69de29 diff --git a/testdata/qmc0_static_target.bin b/testdata/qmc0_static_target.bin new file mode 100644 index 0000000..f3ff0a4 Binary files /dev/null and b/testdata/qmc0_static_target.bin differ