Merge pull request #211 from unlock-music/feature/qmc-v2
Feature: QMC v2 (JS Decoder)
This commit is contained in:
commit
5ec23215fa
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ function MergeUint8Array(array: Uint8Array[]): Uint8Array {
|
|||
* @param {ArrayBuffer} mggBlob 读入的文件 Blob
|
||||
* @return {Promise<Uint8Array|false>}
|
||||
*/
|
||||
export async function DecryptQMCv2(mggBlob: ArrayBuffer) {
|
||||
export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
|
||||
// 初始化模组
|
||||
const QMCCrypto = await QMCCryptoModule();
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5
|
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
yw7xWOyNQ8585Jwx3hjB49QLPKi38F89awnrQ0fq66NT9TDq1ppHNrFqhaDrU5AFk916D58I53h86304GqOFCCyFzBem68DqiXJ81bILEQwG3P3MOnoNzM820kNW9Lv9IJGNn9Xo497p82BLTm4hAX8JLBs0T2pilKvT429sK9jfg508GSk4d047Jxdz5Fou4aa33OkyFRBU3x430mgNBn04Lc9BzXUI2IGYXv3FGa9qE4Vb54kSjVv8ogbg47j3
|
|
@ -0,0 +1 @@
|
|||
eXc3eFdPeU6+3f7GVeF35bMpIEIQj5JWOWt7G+jsR68Hx3BUFBavkTQ8dpPdP0XBIwPe+OfdsnTGVQqPyg3GCtQSrkgA0mwSQdr4DPzKLkEZFX+Cf1V6ChyipOuC6KT37eAxWMdV1UHf9/OCvydr1dc6SWK1ijRUcP6IAHQhiB+mZLay7XXrSPo32WjdBkn9c9sa2SLtI48atj5kfZ4oOq6QGeld2JA3Z+3wwCe6uTHthKaEHY8ufDYodEe3qqrjYpzkdx55pCtxCQa1JiNqFmJigWm4m3CDzhuJ7YqnjbD+mXxLi7BP1+z4L6nccE2h+DGHVqpGjR9+4LBpe4WHB4DrAzVp2qQRRQJxeHd1v88=
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
zGxNk54pKJ0hDkAo80wHE80ycSWQ7z4m4E846zVy2sqCn14F42Y5S7GqeR11WpOV75sDLbE5dFP992t88l0pHy1yAQ49YK6YX6c543drBYLo55Hc4Y0Fyic6LQPiGqu2bG31r8vaq9wS9v63kg0X5VbnOD6RhO4t0RRhk3ajrA7p0iIy027z0L70LZjtw6E18H0D41nz6ASTx71otdF9z1QNC0JmCl51xvnb39zPExEXyKkV47S6QsK5hFh884QJ
|
|
@ -0,0 +1 @@
|
|||
ekd4Tms1NHC53JEDO/AKVyF+I0bj0hHB7CZeoLDGSApaQB9Oo/pJTBGA/RO+nk5RXLXdHsffLiY4e8kt3LNo6qMl7S89vkiSFxx4Uoq4bGDJ7Jc+bYL6lLsa3M4sBvXS4XcPChrMDz+LmrJMGG6ua2fYyIz1d6TCRUBf1JJgCIkBbDAEeMVYc13qApitiz/apGAPmAnveCaDhfD5GxWsF+RfQ2OcnvrnIXe80Feh/0jx763DlsOBI3eIede6t5zYHokWkZmVEF1jMrnlvsgbQK2EzUWMblmLMsTKNILyZazEoKUyulqmyLO/c/KYE+USPOXPcbjlYFmLhSGHK7sQB5aBR153Yp+xh61ooh2NGAA=
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue