diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index 0ff1350..6efa3eb 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,4 +1,4 @@ -import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, StreamCipher} from "./qmc_cipher"; +import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher"; import { AudioMimeType, GetArrayBuffer, @@ -16,7 +16,7 @@ import {DecryptQMCWasm} from "./qmc_wasm"; import iconv from "iconv-lite"; import {DecryptResult} from "@/decrypt/entity"; import {queryAlbumCover} from "@/utils/api"; -import {QmcDecryptKey} from "@/decrypt/qmc_key"; +import {QmcDeriveKey} from "@/decrypt/qmc_key"; interface Handler { ext: string @@ -141,7 +141,8 @@ export class QmcDecoder { size: number decoded: boolean = false audioSize?: number - cipher?: StreamCipher + private static readonly BYTE_COMMA = ','.charCodeAt(0) + cipher?: QmcStreamCipher constructor(file: Uint8Array) { this.file = file @@ -167,16 +168,16 @@ export class QmcDecoder { } private searchKey() { - const last4Byte = this.file.slice(this.size - 4); + const last4Byte = this.file.slice(-4); const textEnc = new TextDecoder() if (textEnc.decode(last4Byte) === 'QTag') { - const sizeBuf = this.file.slice(this.size - 8, this.size - 4) + 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 == ','.charCodeAt(0)) - const keyDec = QmcDecryptKey(rawKey.subarray(0, keyEnd)) + 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); @@ -184,7 +185,7 @@ export class QmcDecoder { if (keySize < 0x300) { this.audioSize = this.size - keySize - 4 const rawKey = this.file.subarray(this.audioSize, this.size - 4) - const keyDec = QmcDecryptKey(rawKey) + const keyDec = QmcDeriveKey(rawKey) this.cipher = new QmcMapCipher(keyDec) } else { this.audioSize = this.size diff --git a/src/decrypt/qmc_cipher.ts b/src/decrypt/qmc_cipher.ts index 1508f5e..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 -]) - -export 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,7 +51,7 @@ export class QmcStaticCipher implements StreamCipher { } } -export class QmcMapCipher implements StreamCipher { +export class QmcMapCipher implements QmcStreamCipher { key: Uint8Array n: number @@ -84,10 +84,10 @@ export class QmcMapCipher implements StreamCipher { } -const FIRST_SEGMENT_SIZE = 0x80; -const SEGMENT_SIZE = 5120 +export class QmcRC4Cipher implements QmcStreamCipher { + private static readonly FIRST_SEGMENT_SIZE = 0x80; + private static readonly SEGMENT_SIZE = 5120 -export class QmcRC4Cipher implements StreamCipher { S: Uint8Array N: number key: Uint8Array @@ -139,23 +139,23 @@ export class QmcRC4Cipher implements StreamCipher { } // Initial segment - if (offset < FIRST_SEGMENT_SIZE) { - const len_segment = Math.min(buf.length, FIRST_SEGMENT_SIZE - offset); + 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 % SEGMENT_SIZE != 0) { - const len_segment = Math.min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), toProcess); + 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 > SEGMENT_SIZE) { - this.encASegment(buf.subarray(processed, processed + SEGMENT_SIZE), offset); - postProcess(SEGMENT_SIZE) + while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) { + this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset); + postProcess(QmcRC4Cipher.SEGMENT_SIZE) } // Last segment (incomplete segment) @@ -168,7 +168,7 @@ export class QmcRC4Cipher implements StreamCipher { private encFirstSegment(buf: Uint8Array, offset: number) { for (let i = 0; i < buf.length; i++) { - buf[i] ^= this.key[this.getSegmentSkip(offset + i)]; + buf[i] ^= this.key[this.getSegmentKey(offset + i)]; } } @@ -178,7 +178,7 @@ export class QmcRC4Cipher implements StreamCipher { // Calculate the number of bytes to skip. // The initial "key" derived from segment id, plus the current offset. - const skipLen = (offset % SEGMENT_SIZE) + this.getSegmentSkip(offset / SEGMENT_SIZE) + const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE) // decrypt the block let j = 0; @@ -194,7 +194,7 @@ export class QmcRC4Cipher implements StreamCipher { } } - private getSegmentSkip(id: number): number { + 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 index 327732a..74e37de 100644 --- a/src/decrypt/qmc_key.test.ts +++ b/src/decrypt/qmc_key.test.ts @@ -1,4 +1,4 @@ -import {QmcDecryptKey, simpleMakeKey,} from "@/decrypt/qmc_key"; +import {QmcDeriveKey, simpleMakeKey} from "@/decrypt/qmc_key"; import fs from "fs"; test("key dec: make simple key", () => { @@ -23,7 +23,7 @@ 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 = QmcDecryptKey(cipherText) + const buf = QmcDeriveKey(cipherText) expect(buf).toStrictEqual(clearText) } diff --git a/src/decrypt/qmc_key.ts b/src/decrypt/qmc_key.ts index 748d4e2..5914b34 100644 --- a/src/decrypt/qmc_key.ts +++ b/src/decrypt/qmc_key.ts @@ -1,5 +1,29 @@ 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++) { @@ -9,8 +33,6 @@ export function simpleMakeKey(salt: number, length: number): number[] { return keyBuf } -const SALT_LEN = 2 -const ZERO_LEN = 7 function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array { if (inBuf.length % 8 != 0) { @@ -83,22 +105,3 @@ function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array { return outBuf } -export function QmcDecryptKey(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) - -} diff --git a/src/decrypt/utils.ts b/src/decrypt/utils.ts index 81692b3..04b2125 100644 --- a/src/decrypt/utils.ts +++ b/src/decrypt/utils.ts @@ -33,7 +33,7 @@ export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean { } export function BytesEqual(a: Uint8Array, b: Uint8Array,): boolean { - if (a.length != b.length) return false + if (a.length !== b.length) return false return a.every((val, idx) => { return val === b[idx]; })