Merge pull request #211 from unlock-music/feature/qmc-v2

Feature: QMC v2 (JS Decoder)
This commit is contained in:
MengYX
2021-12-17 17:58:47 +08:00
committed by GitHub
28 changed files with 691 additions and 54 deletions
+32
View File
@@ -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()
}
})
+72 -13
View File
@@ -1,4 +1,4 @@
import {QmcStaticCipher} from "./qmc_cipher"; import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher";
import { import {
AudioMimeType, AudioMimeType,
GetArrayBuffer, GetArrayBuffer,
@@ -10,12 +10,13 @@ import {
WriteMetaToMp3 WriteMetaToMp3
} from "@/decrypt/utils"; } from "@/decrypt/utils";
import {parseBlob as metaParseBlob} from "music-metadata-browser"; import {parseBlob as metaParseBlob} from "music-metadata-browser";
import {DecryptQMCv2} from "./qmcv2"; import {DecryptQMCWasm} from "./qmc_wasm";
import iconv from "iconv-lite"; import iconv from "iconv-lite";
import {DecryptResult} from "@/decrypt/entity"; import {DecryptResult} from "@/decrypt/entity";
import {queryAlbumCover} from "@/utils/api"; import {queryAlbumCover} from "@/utils/api";
import {QmcDeriveKey} from "@/decrypt/qmc_key";
interface Handler { interface Handler {
ext: string ext: string
@@ -54,22 +55,19 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
const fileBuffer = await GetArrayBuffer(file); const fileBuffer = await GetArrayBuffer(file);
let musicDecoded: Uint8Array | undefined; let musicDecoded: Uint8Array | undefined;
if (version === 2) { if (version === 2 && globalThis.WebAssembly) {
const v2Decrypted = await DecryptQMCv2(fileBuffer); console.log("qmc: using wasm decoder")
const v2Decrypted = await DecryptQMCWasm(fileBuffer);
// 如果 v2 检测失败,降级到 v1 再尝试一次 // 如果 v2 检测失败,降级到 v1 再尝试一次
if (v2Decrypted) { if (v2Decrypted) {
musicDecoded = v2Decrypted; musicDecoded = v2Decrypted;
} else {
version = 1;
} }
} }
if (!musicDecoded) {
if (version === 1) { // may throw error
const seed = new QmcStaticCipher(); console.log("qmc: using js decoder")
musicDecoded = new Uint8Array(fileBuffer) const d = new QmcDecoder(new Uint8Array(fileBuffer))
seed.decrypt(musicDecoded, 0); musicDecoded = d.decrypt()
} else if (!musicDecoded) {
throw new Error(`解密失败: ${raw_ext}`);
} }
const ext = SniffAudioExt(musicDecoded, handler.ext); const ext = SniffAudioExt(musicDecoded, handler.ext);
@@ -137,3 +135,64 @@ async function getCoverImage(title: string, artist?: string, album?: string): Pr
} }
return "" 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()
}
}
}
}
+89 -1
View File
@@ -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) ", () => { test("static cipher [0x7ff8,0x8000) ", () => {
const expected = new Uint8Array([ const expected = new Uint8Array([
@@ -25,3 +26,90 @@ test("static cipher [0,0x10) ", () => {
expect(buf).toStrictEqual(expected) 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))
}
})
+187 -38
View File
@@ -1,47 +1,47 @@
const staticCipherBox = new Uint8Array([ export interface QmcStreamCipher {
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 {
decrypt(buf: Uint8Array, offset: number): void 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) { public getMask(offset: number) {
if (offset > 0x7FFF) offset %= 0x7FFF if (offset > 0x7FFF) offset %= 0x7FFF
return staticCipherBox[(offset * offset + 27) & 0xff] return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff]
} }
public decrypt(buf: Uint8Array, offset: number) { 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
}
}
+30
View File
@@ -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)
}
})
+107
View File
@@ -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 * @param {ArrayBuffer} mggBlob Blob
* @return {Promise<Uint8Array|false>} * @return {Promise<Uint8Array|false>}
*/ */
export async function DecryptQMCv2(mggBlob: ArrayBuffer) { export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
// 初始化模组 // 初始化模组
const QMCCrypto = await QMCCryptoModule(); const QMCCrypto = await QMCCryptoModule();
+8 -1
View File
@@ -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 { export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string {
if (BytesHasPrefix(data, MP3_HEADER)) return "mp3" if (BytesHasPrefix(data, MP3_HEADER)) return "mp3"
if (BytesHasPrefix(data, FLAC_HEADER)) return "flac" if (BytesHasPrefix(data, FLAC_HEADER)) return "flac"
if (BytesHasPrefix(data, OGG_HEADER)) return "ogg" if (BytesHasPrefix(data, OGG_HEADER)) return "ogg"
if (data.length >= 4 + M4A_HEADER.length && 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, WAV_HEADER)) return "wav"
if (BytesHasPrefix(data, WMA_HEADER)) return "wma" if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
if (BytesHasPrefix(data, AAC_HEADER)) return "aac" if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
+77
View File
@@ -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)
}
})
+82
View File
@@ -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)
}
}
+1
View File
@@ -0,0 +1 @@
dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5
+1
View File
@@ -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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
yw7xWOyNQ8585Jwx3hjB49QLPKi38F89awnrQ0fq66NT9TDq1ppHNrFqhaDrU5AFk916D58I53h86304GqOFCCyFzBem68DqiXJ81bILEQwG3P3MOnoNzM820kNW9Lv9IJGNn9Xo497p82BLTm4hAX8JLBs0T2pilKvT429sK9jfg508GSk4d047Jxdz5Fou4aa33OkyFRBU3x430mgNBn04Lc9BzXUI2IGYXv3FGa9qE4Vb54kSjVv8ogbg47j3
+1
View File
@@ -0,0 +1 @@
eXc3eFdPeU6+3f7GVeF35bMpIEIQj5JWOWt7G+jsR68Hx3BUFBavkTQ8dpPdP0XBIwPe+OfdsnTGVQqPyg3GCtQSrkgA0mwSQdr4DPzKLkEZFX+Cf1V6ChyipOuC6KT37eAxWMdV1UHf9/OCvydr1dc6SWK1ijRUcP6IAHQhiB+mZLay7XXrSPo32WjdBkn9c9sa2SLtI48atj5kfZ4oOq6QGeld2JA3Z+3wwCe6uTHthKaEHY8ufDYodEe3qqrjYpzkdx55pCtxCQa1JiNqFmJigWm4m3CDzhuJ7YqnjbD+mXxLi7BP1+z4L6nccE2h+DGHVqpGjR9+4LBpe4WHB4DrAzVp2qQRRQJxeHd1v88=
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
zGxNk54pKJ0hDkAo80wHE80ycSWQ7z4m4E846zVy2sqCn14F42Y5S7GqeR11WpOV75sDLbE5dFP992t88l0pHy1yAQ49YK6YX6c543drBYLo55Hc4Y0Fyic6LQPiGqu2bG31r8vaq9wS9v63kg0X5VbnOD6RhO4t0RRhk3ajrA7p0iIy027z0L70LZjtw6E18H0D41nz6ASTx71otdF9z1QNC0JmCl51xvnb39zPExEXyKkV47S6QsK5hFh884QJ
+1
View File
@@ -0,0 +1 @@
ekd4Tms1NHC53JEDO/AKVyF+I0bj0hHB7CZeoLDGSApaQB9Oo/pJTBGA/RO+nk5RXLXdHsffLiY4e8kt3LNo6qMl7S89vkiSFxx4Uoq4bGDJ7Jc+bYL6lLsa3M4sBvXS4XcPChrMDz+LmrJMGG6ua2fYyIz1d6TCRUBf1JJgCIkBbDAEeMVYc13qApitiz/apGAPmAnveCaDhfD5GxWsF+RfQ2OcnvrnIXe80Feh/0jx763DlsOBI3eIede6t5zYHokWkZmVEF1jMrnlvsgbQK2EzUWMblmLMsTKNILyZazEoKUyulqmyLO/c/KYE+USPOXPcbjlYFmLhSGHK7sQB5aBR153Yp+xh61ooh2NGAA=
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
View File
BIN
View File
Binary file not shown.