diff --git a/src/decrypt/kgm.ts b/src/decrypt/kgm.ts index ca3afa6..b6553c9 100644 --- a/src/decrypt/kgm.ts +++ b/src/decrypt/kgm.ts @@ -9,7 +9,6 @@ import { import { parseBlob as metaParseBlob } from 'music-metadata-browser'; import { DecryptResult } from '@/decrypt/entity'; import { DecryptKgmWasm } from '@/decrypt/kgm_wasm'; -import { decryptKgmByteAtOffsetV2, decryptVprByteAtOffset } from '@jixun/kugou-crypto/dist/utils/decryptionHelper'; //prettier-ignore const VprHeader = [ @@ -29,33 +28,14 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string) } else { if (!BytesHasPrefix(new Uint8Array(oriData), KgmHeader)) throw Error('Not a valid kgm(a) file!'); } - let musicDecoded: Uint8Array | undefined; + let musicDecoded = new Uint8Array(); if (globalThis.WebAssembly) { - console.log('kgm: using wasm decoder'); - const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext); if (kgmDecrypted.success) { musicDecoded = kgmDecrypted.data; console.log('kgm wasm decoder suceeded'); } else { - console.warn('KgmWasm failed with error %s', kgmDecrypted.error || '(unknown error)'); - } - } - - if (!musicDecoded) { - musicDecoded = new Uint8Array(oriData); - let bHeaderLen = new DataView(musicDecoded.slice(0x10, 0x14).buffer); - let headerLen = bHeaderLen.getUint32(0, true); - - let key1 = Array.from(musicDecoded.slice(0x1c, 0x2c)); - key1.push(0); - - musicDecoded = musicDecoded.slice(headerLen); - let dataLen = musicDecoded.length; - - const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2; - for (let i = 0; i < dataLen; i++) { - musicDecoded[i] = decryptByte(musicDecoded[i], key1, i); + throw new Error(kgmDecrypted.error || '(unknown error)'); } } diff --git a/src/decrypt/qmc.test.ts b/src/decrypt/qmc.test.ts deleted file mode 100644 index 73156bd..0000000 --- a/src/decrypt/qmc.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -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_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 3154f90..b5b5401 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,8 +1,6 @@ -import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher'; import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils'; import { DecryptResult } from '@/decrypt/entity'; -import { QmcDeriveKey } from '@/decrypt/qmc_key'; import { DecryptQmcWasm } from '@/decrypt/qmc_wasm'; import { extractQQMusicMeta } from '@/utils/qm_meta'; @@ -18,7 +16,7 @@ export const HandlerMap: { [key: string]: Handler } = { mgg1: { ext: 'ogg', version: 2 }, mflac: { ext: 'flac', version: 2 }, mflac0: { ext: 'flac', version: 2 }, - mmp4: { ext: 'mmp4', version: 2 }, + mmp4: { ext: 'mp4', version: 2 }, // qmcflac / qmcogg: // 有可能是 v2 加密但混用同一个后缀名。 @@ -52,12 +50,10 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) let { version } = handler; const fileBuffer = await GetArrayBuffer(file); - let musicDecoded: Uint8Array | undefined; + let musicDecoded = new Uint8Array(); let musicID: number | string | undefined; if (version === 2 && globalThis.WebAssembly) { - console.log('qmc: using wasm decoder'); - const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext); // 若 v2 检测失败,降级到 v1 再尝试一次 if (v2Decrypted.success) { @@ -65,18 +61,10 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) musicID = v2Decrypted.songId; console.log('qmc wasm decoder suceeded'); } else { - console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(unknown error)'); + throw new Error(v2Decrypted.error || '(unknown error)'); } } - if (!musicDecoded) { - // may throw error - console.log('qmc: using js decoder'); - const d = new QmcDecoder(new Uint8Array(fileBuffer)); - musicDecoded = d.decrypt(); - musicID = d.songID; - } - const ext = SniffAudioExt(musicDecoded, handler.ext); const mime = AudioMimeType[ext]; @@ -98,88 +86,3 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) mime: mime, }; } - -export class QmcDecoder { - private static readonly BYTE_COMMA = ','.charCodeAt(0); - private readonly file: Uint8Array; - private readonly size: number; - private decoded: boolean = false; - private audioSize?: number; - private cipher?: QmcStreamCipher; - - public constructor(file: Uint8Array) { - this.file = file; - this.size = file.length; - this.searchKey(); - } - - private _songID?: number; - - public get songID() { - return this._songID; - } - - public 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) === 'STag') { - throw new Error('文件中没有写入密钥,无法解锁,请降级App并重试'); - } else 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); - if (keyEnd < 0) { - throw new Error('invalid key: search raw key failed'); - } - this.setCipher(rawKey.subarray(0, keyEnd)); - - const idBuf = rawKey.subarray(keyEnd + 1); - const idEnd = idBuf.findIndex((v) => v == QmcDecoder.BYTE_COMMA); - if (keyEnd < 0) { - throw new Error('invalid key: search song id failed'); - } - this._songID = parseInt(textEnc.decode(idBuf.subarray(0, idEnd)), 10); - } else { - const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset); - const keySize = sizeView.getUint32(0, true); - if (keySize < 0x400) { - this.audioSize = this.size - keySize - 4; - const rawKey = this.file.subarray(this.audioSize, this.size - 4); - this.setCipher(rawKey); - } else { - this.audioSize = this.size; - this.cipher = new QmcStaticCipher(); - } - } - } - - private setCipher(keyRaw: Uint8Array) { - const keyDec = QmcDeriveKey(keyRaw); - if (keyDec.length > 300) { - this.cipher = new QmcRC4Cipher(keyDec); - } else { - this.cipher = new QmcMapCipher(keyDec); - } - } -} diff --git a/src/decrypt/qmc_cipher.test.ts b/src/decrypt/qmc_cipher.test.ts deleted file mode 100644 index 0f1e0b7..0000000 --- a/src/decrypt/qmc_cipher.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher'; -import fs from 'fs'; - -test('static cipher [0x7ff8,0x8000) ', () => { - //prettier-ignore - const expected = new Uint8Array([ - 0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A, - 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8, - ]) - - const c = new QmcStaticCipher(); - const buf = new Uint8Array(16); - c.decrypt(buf, 0x7ff8); - - expect(buf).toStrictEqual(expected); -}); - -test('static cipher [0,0x10) ', () => { - //prettier-ignore - const expected = new Uint8Array([ - 0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, - 0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00, - ]) - - const c = new QmcStaticCipher(); - const buf = new Uint8Array(16); - c.decrypt(buf, 0); - - expect(buf).toStrictEqual(expected); -}); - -test('map cipher: get mask', () => { - //prettier-ignore - 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', 'mflac_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', 'mflac_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', 'mflac_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', 'mflac_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 deleted file mode 100644 index d0d3683..0000000 --- a/src/decrypt/qmc_cipher.ts +++ /dev/null @@ -1,199 +0,0 @@ -export interface QmcStreamCipher { - decrypt(buf: Uint8Array, offset: number): void; -} - -export class QmcStaticCipher implements QmcStreamCipher { - //prettier-ignore - 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 QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff]; - } - - public decrypt(buf: Uint8Array, offset: number) { - for (let i = 0; i < buf.length; i++) { - buf[i] ^= this.getMask(offset + i); - } - } -} - -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) >>> 0; - 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(Math.floor(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 = Math.floor((this.hash / ((id + 1) * seed)) * 100.0); - return idx % this.N; - } -} diff --git a/src/decrypt/qmc_key.test.ts b/src/decrypt/qmc_key.test.ts deleted file mode 100644 index 2ec1e96..0000000 --- a/src/decrypt/qmc_key.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -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', 'mflac_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 deleted file mode 100644 index b23b15f..0000000 --- a/src/decrypt/qmc_key.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { TeaCipher } from '@/utils/tea'; - -const SALT_LEN = 2; -const ZERO_LEN = 7; - -export function QmcDeriveKey(raw: Uint8Array): Uint8Array { - const textDec = new TextDecoder(); - let rawDec = Buffer.from(textDec.decode(raw), 'base64'); - let n = rawDec.length; - if (n < 16) { - throw Error('key length is too short'); - } - - rawDec = decryptV2Key(rawDec); - - 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; -} - -const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ]) -const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ]) - -function decryptV2Key(key: Buffer): Buffer -{ - const textEnc = new TextDecoder(); - if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') { - return key; - } - - let out = decryptTencentTea(key.slice(18), mixKey1); - out = decryptTencentTea(out, mixKey2); - const textDec = new TextDecoder(); - const keyDec = Buffer.from(textDec.decode(out), 'base64'); - let n = keyDec.length; - if (n < 16) { - throw Error('EncV2 key decode failed'); - } - - return keyDec; -} - -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/qmccache.ts b/src/decrypt/qmccache.ts index 6a57a94..0b6582b 100644 --- a/src/decrypt/qmccache.ts +++ b/src/decrypt/qmccache.ts @@ -17,7 +17,7 @@ import { parseBlob as metaParseBlob } from 'music-metadata-browser'; export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise { const buffer = await GetArrayBuffer(file); - let musicDecoded: Uint8Array | undefined; + let musicDecoded = new Uint8Array(); if (globalThis.WebAssembly) { console.log('qmc: using wasm decoder'); @@ -27,19 +27,10 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) musicDecoded = qmcDecrypted.data; console.log('qmc wasm decoder suceeded'); } else { - console.warn('QmcWasm failed with error %s', qmcDecrypted.error || '(unknown error)'); + throw new Error(qmcDecrypted.error || '(unknown error)'); } } - if (!musicDecoded) { - musicDecoded = new Uint8Array(buffer); - let length = musicDecoded.length; - for (let i = 0; i < length; i++) { - let byte = musicDecoded[i] ^ 0xf4; // xor 0xf4 - byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2 - musicDecoded[i] = byte; - } - } let ext = SniffAudioExt(musicDecoded, ''); const newName = SplitFilename(raw_filename); let audioBlob: Blob; diff --git a/src/utils/tea.test.ts b/src/utils/tea.test.ts deleted file mode 100644 index bb3fbee..0000000 --- a/src/utils/tea.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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', () => { - // prettier-ignore - 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(); -}); - -// prettier-ignore -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 deleted file mode 100644 index 0a93d6a..0000000 --- a/src/utils/tea.ts +++ /dev/null @@ -1,80 +0,0 @@ -// 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); - } -}