Merge pull request #211 from unlock-music/feature/qmc-v2
Feature: QMC v2 (JS Decoder)
This commit is contained in:
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
* @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();
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
dRzX3p5ZYqAlp7lLSs9Zr0rw1iEZy23bB670x4ch2w97x14Zwpk1UXbKU4C2sOS7uZ0NB5QM7ve9GnSrr2JHxP74hVNONwVV77CdOOVb807317KvtI5Yd6h08d0c5W88rdV46C235YGDjUSZj5314YTzy0b6vgh4102P7E273r911Nl464XV83Hr00rkAHkk791iMGSJH95GztN28u2Nv5s9Xx38V69o4a8aIXxbx0g1EM0623OEtbtO9zsqCJfj6MhU7T8iVS6M3q19xhq6707E6r7wzPO6Yp4BwBmgg4F95Lfl0vyF7YO6699tb5LMnr7iFx29o98hoh3O3Rd8h9Juu8P1wG7vdnO5YtRlykhUluYQblNn7XwjBJ53HAyKVraWN5dG7pv7OMl1s0RykPh0p23qfYzAAMkZ1M422pEd07TA9OCKD1iybYxWH06xj6A8mzmcnYGT9P1a5Ytg2EF5LG3IknL2r3AUz99Y751au6Cr401mfAWK68WyEBe5
|
||||||
Vendored
+1
@@ -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
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
yw7xWOyNQ8585Jwx3hjB49QLPKi38F89awnrQ0fq66NT9TDq1ppHNrFqhaDrU5AFk916D58I53h86304GqOFCCyFzBem68DqiXJ81bILEQwG3P3MOnoNzM820kNW9Lv9IJGNn9Xo497p82BLTm4hAX8JLBs0T2pilKvT429sK9jfg508GSk4d047Jxdz5Fou4aa33OkyFRBU3x430mgNBn04Lc9BzXUI2IGYXv3FGa9qE4Vb54kSjVv8ogbg47j3
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
eXc3eFdPeU6+3f7GVeF35bMpIEIQj5JWOWt7G+jsR68Hx3BUFBavkTQ8dpPdP0XBIwPe+OfdsnTGVQqPyg3GCtQSrkgA0mwSQdr4DPzKLkEZFX+Cf1V6ChyipOuC6KT37eAxWMdV1UHf9/OCvydr1dc6SWK1ijRUcP6IAHQhiB+mZLay7XXrSPo32WjdBkn9c9sa2SLtI48atj5kfZ4oOq6QGeld2JA3Z+3wwCe6uTHthKaEHY8ufDYodEe3qqrjYpzkdx55pCtxCQa1JiNqFmJigWm4m3CDzhuJ7YqnjbD+mXxLi7BP1+z4L6nccE2h+DGHVqpGjR9+4LBpe4WHB4DrAzVp2qQRRQJxeHd1v88=
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
zGxNk54pKJ0hDkAo80wHE80ycSWQ7z4m4E846zVy2sqCn14F42Y5S7GqeR11WpOV75sDLbE5dFP992t88l0pHy1yAQ49YK6YX6c543drBYLo55Hc4Y0Fyic6LQPiGqu2bG31r8vaq9wS9v63kg0X5VbnOD6RhO4t0RRhk3ajrA7p0iIy027z0L70LZjtw6E18H0D41nz6ASTx71otdF9z1QNC0JmCl51xvnb39zPExEXyKkV47S6QsK5hFh884QJ
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
ekd4Tms1NHC53JEDO/AKVyF+I0bj0hHB7CZeoLDGSApaQB9Oo/pJTBGA/RO+nk5RXLXdHsffLiY4e8kt3LNo6qMl7S89vkiSFxx4Uoq4bGDJ7Jc+bYL6lLsa3M4sBvXS4XcPChrMDz+LmrJMGG6ua2fYyIz1d6TCRUBf1JJgCIkBbDAEeMVYc13qApitiz/apGAPmAnveCaDhfD5GxWsF+RfQ2OcnvrnIXe80Feh/0jx763DlsOBI3eIede6t5zYHokWkZmVEF1jMrnlvsgbQK2EzUWMblmLMsTKNILyZazEoKUyulqmyLO/c/KYE+USPOXPcbjlYFmLhSGHK7sQB5aBR153Yp+xh61ooh2NGAA=
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user