delete js implementations
parent
c5b25eb3b5
commit
8475a5bc65
@ -1,29 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { QmcDecoder } from '@/decrypt/qmc';
|
|
||||||
import { BytesEqual } from '@/decrypt/utils';
|
|
||||||
|
|
||||||
function loadTestDataDecoder(name: string): {
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
|
|
||||||
const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
|
|
||||||
const cipherText = new Uint8Array(cipherBody.length + cipherSuffix.length);
|
|
||||||
cipherText.set(cipherBody);
|
|
||||||
cipherText.set(cipherSuffix, cipherBody.length);
|
|
||||||
return {
|
|
||||||
cipherText,
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('qmc: real file', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { clearText, cipherText } = loadTestDataDecoder(name);
|
|
||||||
const c = new QmcDecoder(cipherText);
|
|
||||||
const buf = c.decrypt();
|
|
||||||
|
|
||||||
expect(BytesEqual(buf, clearText)).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,117 +0,0 @@
|
|||||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
test('static cipher [0x7ff8,0x8000) ', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
|
|
||||||
0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
|
|
||||||
])
|
|
||||||
|
|
||||||
const c = new QmcStaticCipher();
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
c.decrypt(buf, 0x7ff8);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('static cipher [0,0x10) ', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
|
|
||||||
0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
|
|
||||||
])
|
|
||||||
|
|
||||||
const c = new QmcStaticCipher();
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('map cipher: get mask', () => {
|
|
||||||
//prettier-ignore
|
|
||||||
const expected = new Uint8Array([
|
|
||||||
0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
|
|
||||||
0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
|
|
||||||
])
|
|
||||||
const key = new Uint8Array(256);
|
|
||||||
for (let i = 0; i < 256; i++) key[i] = i;
|
|
||||||
const buf = new Uint8Array(16);
|
|
||||||
|
|
||||||
const c = new QmcMapCipher(key);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
expect(buf).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadTestDataCipher(name: string): {
|
|
||||||
key: Uint8Array;
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
key: fs.readFileSync(`testdata/${name}_key.bin`),
|
|
||||||
cipherText: fs.readFileSync(`testdata/${name}_raw.bin`),
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('map cipher: real file', async () => {
|
|
||||||
const cases = ['mflac_map', 'mgg_map'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcMapCipher(key);
|
|
||||||
|
|
||||||
c.decrypt(cipherText, 0);
|
|
||||||
|
|
||||||
expect(cipherText).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: real file', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
c.decrypt(cipherText, 0);
|
|
||||||
|
|
||||||
expect(cipherText).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: first segment', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(0, 128);
|
|
||||||
c.decrypt(buf, 0);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(0, 128));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: align block (128~5120)', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(128, 5120);
|
|
||||||
c.decrypt(buf, 128);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(128, 5120));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rc4 cipher: simple block (5120~10240)', async () => {
|
|
||||||
const cases = ['mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
|
||||||
const c = new QmcRC4Cipher(key);
|
|
||||||
|
|
||||||
const buf = cipherText.slice(5120, 10240);
|
|
||||||
c.decrypt(buf, 5120);
|
|
||||||
expect(buf).toStrictEqual(clearText.slice(5120, 10240));
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,199 +0,0 @@
|
|||||||
export interface QmcStreamCipher {
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcStaticCipher implements QmcStreamCipher {
|
|
||||||
//prettier-ignore
|
|
||||||
private static readonly staticCipherBox: Uint8Array = new Uint8Array([
|
|
||||||
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
|
|
||||||
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
|
|
||||||
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
|
|
||||||
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
|
|
||||||
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
|
|
||||||
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
|
|
||||||
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
|
|
||||||
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
|
|
||||||
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
|
|
||||||
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
|
|
||||||
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
|
|
||||||
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
|
|
||||||
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
|
|
||||||
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
|
|
||||||
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
|
|
||||||
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
|
|
||||||
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
|
|
||||||
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
|
|
||||||
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
|
|
||||||
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
|
|
||||||
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
|
|
||||||
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
|
|
||||||
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
|
|
||||||
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
|
|
||||||
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
|
|
||||||
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
|
|
||||||
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
|
|
||||||
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
|
|
||||||
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
|
|
||||||
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
|
|
||||||
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
|
|
||||||
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8
|
|
||||||
])
|
|
||||||
|
|
||||||
public getMask(offset: number) {
|
|
||||||
if (offset > 0x7fff) offset %= 0x7fff;
|
|
||||||
return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
|
|
||||||
}
|
|
||||||
|
|
||||||
public decrypt(buf: Uint8Array, offset: number) {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.getMask(offset + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcMapCipher implements QmcStreamCipher {
|
|
||||||
key: Uint8Array;
|
|
||||||
n: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array) {
|
|
||||||
if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.n = key.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static rotate(value: number, bits: number) {
|
|
||||||
let rotate = (bits + 4) % 8;
|
|
||||||
let left = value << rotate;
|
|
||||||
let right = value >> rotate;
|
|
||||||
return (left | right) & 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.getMask(offset + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMask(offset: number) {
|
|
||||||
if (offset > 0x7fff) offset %= 0x7fff;
|
|
||||||
|
|
||||||
const idx = (offset * offset + 71214) % this.n;
|
|
||||||
return QmcMapCipher.rotate(this.key[idx], idx & 0x7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QmcRC4Cipher implements QmcStreamCipher {
|
|
||||||
private static readonly FIRST_SEGMENT_SIZE = 0x80;
|
|
||||||
private static readonly SEGMENT_SIZE = 5120;
|
|
||||||
|
|
||||||
S: Uint8Array;
|
|
||||||
N: number;
|
|
||||||
key: Uint8Array;
|
|
||||||
hash: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array) {
|
|
||||||
if (key.length == 0) {
|
|
||||||
throw Error('invalid key size');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.N = key.length;
|
|
||||||
|
|
||||||
// init seed box
|
|
||||||
this.S = new Uint8Array(this.N);
|
|
||||||
for (let i = 0; i < this.N; ++i) {
|
|
||||||
this.S[i] = i & 0xff;
|
|
||||||
}
|
|
||||||
let j = 0;
|
|
||||||
for (let i = 0; i < this.N; ++i) {
|
|
||||||
j = (this.S[i] + j + this.key[i % this.N]) % this.N;
|
|
||||||
[this.S[i], this.S[j]] = [this.S[j], this.S[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// init hash base
|
|
||||||
this.hash = 1;
|
|
||||||
for (let i = 0; i < this.N; i++) {
|
|
||||||
let value = this.key[i];
|
|
||||||
|
|
||||||
// ignore if key char is '\x00'
|
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
const next_hash = (this.hash * value) >>> 0;
|
|
||||||
if (next_hash == 0 || next_hash <= this.hash) break;
|
|
||||||
|
|
||||||
this.hash = next_hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(buf: Uint8Array, offset: number): void {
|
|
||||||
let toProcess = buf.length;
|
|
||||||
let processed = 0;
|
|
||||||
const postProcess = (len: number): boolean => {
|
|
||||||
toProcess -= len;
|
|
||||||
processed += len;
|
|
||||||
offset += len;
|
|
||||||
return toProcess == 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial segment
|
|
||||||
if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
|
|
||||||
const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
|
|
||||||
this.encFirstSegment(buf.subarray(0, len_segment), offset);
|
|
||||||
if (postProcess(len_segment)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// align segment
|
|
||||||
if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
|
|
||||||
const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
|
|
||||||
this.encASegment(buf.subarray(processed, processed + len_segment), offset);
|
|
||||||
if (postProcess(len_segment)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch process segments
|
|
||||||
while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
|
|
||||||
this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
|
|
||||||
postProcess(QmcRC4Cipher.SEGMENT_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last segment (incomplete segment)
|
|
||||||
if (toProcess > 0) {
|
|
||||||
this.encASegment(buf.subarray(processed), offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private encFirstSegment(buf: Uint8Array, offset: number) {
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
buf[i] ^= this.key[this.getSegmentKey(offset + i)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private encASegment(buf: Uint8Array, offset: number) {
|
|
||||||
// Initialise a new seed box
|
|
||||||
const S = this.S.slice(0);
|
|
||||||
|
|
||||||
// Calculate the number of bytes to skip.
|
|
||||||
// The initial "key" derived from segment id, plus the current offset.
|
|
||||||
const skipLen =
|
|
||||||
(offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(Math.floor(offset / QmcRC4Cipher.SEGMENT_SIZE));
|
|
||||||
|
|
||||||
// decrypt the block
|
|
||||||
let j = 0;
|
|
||||||
let k = 0;
|
|
||||||
for (let i = -skipLen; i < buf.length; i++) {
|
|
||||||
j = (j + 1) % this.N;
|
|
||||||
k = (S[j] + k) % this.N;
|
|
||||||
[S[k], S[j]] = [S[j], S[k]];
|
|
||||||
|
|
||||||
if (i >= 0) {
|
|
||||||
buf[i] ^= S[(S[j] + S[k]) % this.N];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSegmentKey(id: number): number {
|
|
||||||
const seed = this.key[id % this.N];
|
|
||||||
const idx = Math.floor((this.hash / ((id + 1) * seed)) * 100.0);
|
|
||||||
return idx % this.N;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
test('key dec: make simple key', () => {
|
|
||||||
expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadTestDataKeyDecrypt(name: string): {
|
|
||||||
cipherText: Uint8Array;
|
|
||||||
clearText: Uint8Array;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
|
|
||||||
clearText: fs.readFileSync(`testdata/${name}_key.bin`),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('key dec: real file', async () => {
|
|
||||||
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4', 'mflac_rc4'];
|
|
||||||
for (const name of cases) {
|
|
||||||
const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
|
|
||||||
const buf = QmcDeriveKey(cipherText);
|
|
||||||
|
|
||||||
expect(buf).toStrictEqual(clearText);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,127 +0,0 @@
|
|||||||
import { TeaCipher } from '@/utils/tea';
|
|
||||||
|
|
||||||
const SALT_LEN = 2;
|
|
||||||
const ZERO_LEN = 7;
|
|
||||||
|
|
||||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
|
|
||||||
const textDec = new TextDecoder();
|
|
||||||
let rawDec = Buffer.from(textDec.decode(raw), 'base64');
|
|
||||||
let n = rawDec.length;
|
|
||||||
if (n < 16) {
|
|
||||||
throw Error('key length is too short');
|
|
||||||
}
|
|
||||||
|
|
||||||
rawDec = decryptV2Key(rawDec);
|
|
||||||
|
|
||||||
const simpleKey = simpleMakeKey(106, 8);
|
|
||||||
let teaKey = new Uint8Array(16);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
teaKey[i << 1] = simpleKey[i];
|
|
||||||
teaKey[(i << 1) + 1] = rawDec[i];
|
|
||||||
}
|
|
||||||
const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
|
|
||||||
rawDec.set(sub, 8);
|
|
||||||
return rawDec.subarray(0, 8 + sub.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleMakeKey exported only for unit test
|
|
||||||
export function simpleMakeKey(salt: number, length: number): number[] {
|
|
||||||
const keyBuf: number[] = [];
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const tmp = Math.tan(salt + i * 0.1);
|
|
||||||
keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
|
|
||||||
}
|
|
||||||
return keyBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ])
|
|
||||||
const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ])
|
|
||||||
|
|
||||||
function decryptV2Key(key: Buffer): Buffer
|
|
||||||
{
|
|
||||||
const textEnc = new TextDecoder();
|
|
||||||
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
let out = decryptTencentTea(key.slice(18), mixKey1);
|
|
||||||
out = decryptTencentTea(out, mixKey2);
|
|
||||||
const textDec = new TextDecoder();
|
|
||||||
const keyDec = Buffer.from(textDec.decode(out), 'base64');
|
|
||||||
let n = keyDec.length;
|
|
||||||
if (n < 16) {
|
|
||||||
throw Error('EncV2 key decode failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyDec;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
|
|
||||||
if (inBuf.length % 8 != 0) {
|
|
||||||
throw Error('inBuf size not a multiple of the block size');
|
|
||||||
}
|
|
||||||
if (inBuf.length < 16) {
|
|
||||||
throw Error('inBuf size too small');
|
|
||||||
}
|
|
||||||
|
|
||||||
const blk = new TeaCipher(key, 32);
|
|
||||||
|
|
||||||
const tmpBuf = new Uint8Array(8);
|
|
||||||
const tmpView = new DataView(tmpBuf.buffer);
|
|
||||||
|
|
||||||
blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8));
|
|
||||||
|
|
||||||
const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
|
|
||||||
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
|
|
||||||
const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
|
|
||||||
const outBuf = new Uint8Array(outLen);
|
|
||||||
|
|
||||||
let ivPrev = new Uint8Array(8);
|
|
||||||
let ivCur = inBuf.slice(0, 8); // init iv
|
|
||||||
let inBufPos = 8;
|
|
||||||
|
|
||||||
// 跳过 Padding Len 和 Padding
|
|
||||||
let tmpIdx = 1 + nPadLen;
|
|
||||||
|
|
||||||
// CBC IV 处理
|
|
||||||
const cryptBlock = () => {
|
|
||||||
ivPrev = ivCur;
|
|
||||||
ivCur = inBuf.slice(inBufPos, inBufPos + 8);
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
tmpBuf[j] ^= ivCur[j];
|
|
||||||
}
|
|
||||||
blk.decrypt(tmpView, tmpView);
|
|
||||||
inBufPos += 8;
|
|
||||||
tmpIdx = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳过 Salt
|
|
||||||
for (let i = 1; i <= SALT_LEN; ) {
|
|
||||||
if (tmpIdx < 8) {
|
|
||||||
tmpIdx++;
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
cryptBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 还原明文
|
|
||||||
let outBufPos = 0;
|
|
||||||
while (outBufPos < outLen) {
|
|
||||||
if (tmpIdx < 8) {
|
|
||||||
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
|
|
||||||
outBufPos++;
|
|
||||||
tmpIdx++;
|
|
||||||
} else {
|
|
||||||
cryptBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验Zero
|
|
||||||
for (let i = 1; i <= ZERO_LEN; i++) {
|
|
||||||
if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
|
|
||||||
throw Error('zero check failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outBuf;
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
// Copyright 2021 MengYX. All rights reserved.
|
|
||||||
//
|
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in https://go.dev/LICENSE.
|
|
||||||
|
|
||||||
import { TeaCipher } from '@/utils/tea';
|
|
||||||
|
|
||||||
test('key size', () => {
|
|
||||||
// prettier-ignore
|
|
||||||
const testKey = new Uint8Array([
|
|
||||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
|
||||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
|
|
||||||
0x00,
|
|
||||||
])
|
|
||||||
expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => new TeaCipher(testKey)).toThrow();
|
|
||||||
|
|
||||||
expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const teaTests = [
|
|
||||||
// These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
|
|
||||||
{
|
|
||||||
rounds: TeaCipher.numRounds,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
||||||
cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rounds: TeaCipher.numRounds,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
|
||||||
cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rounds: 16,
|
|
||||||
key: new Uint8Array([
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]),
|
|
||||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
||||||
cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
test('rounds', () => {
|
|
||||||
const tt = teaTests[0];
|
|
||||||
expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('encrypt & decrypt', () => {
|
|
||||||
for (const tt of teaTests) {
|
|
||||||
const c = new TeaCipher(tt.key, tt.rounds);
|
|
||||||
|
|
||||||
const buf = new Uint8Array(8);
|
|
||||||
const bufView = new DataView(buf.buffer);
|
|
||||||
|
|
||||||
c.encrypt(bufView, new DataView(tt.plainText.buffer));
|
|
||||||
expect(buf).toStrictEqual(tt.cipherText);
|
|
||||||
|
|
||||||
c.decrypt(bufView, new DataView(tt.cipherText.buffer));
|
|
||||||
expect(buf).toStrictEqual(tt.plainText);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,80 +0,0 @@
|
|||||||
// Copyright 2021 MengYX. All rights reserved.
|
|
||||||
//
|
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in https://go.dev/LICENSE.
|
|
||||||
|
|
||||||
// TeaCipher is a typescript port to golang.org/x/crypto/tea
|
|
||||||
|
|
||||||
// Package tea implements the TEA algorithm, as defined in Needham and
|
|
||||||
// Wheeler's 1994 technical report, “TEA, a Tiny Encryption Algorithm”. See
|
|
||||||
// http://www.cix.co.uk/~klockstone/tea.pdf for details.
|
|
||||||
//
|
|
||||||
// TEA is a legacy cipher and its short block size makes it vulnerable to
|
|
||||||
// birthday bound attacks (see https://sweet32.info). It should only be used
|
|
||||||
// where compatibility with legacy systems, not security, is the goal.
|
|
||||||
|
|
||||||
export class TeaCipher {
|
|
||||||
// BlockSize is the size of a TEA block, in bytes.
|
|
||||||
static readonly BlockSize = 8;
|
|
||||||
|
|
||||||
// KeySize is the size of a TEA key, in bytes.
|
|
||||||
static readonly KeySize = 16;
|
|
||||||
|
|
||||||
// delta is the TEA key schedule constant.
|
|
||||||
static readonly delta = 0x9e3779b9;
|
|
||||||
|
|
||||||
// numRounds 64 is the standard number of rounds in TEA.
|
|
||||||
static readonly numRounds = 64;
|
|
||||||
|
|
||||||
k0: number;
|
|
||||||
k1: number;
|
|
||||||
k2: number;
|
|
||||||
k3: number;
|
|
||||||
rounds: number;
|
|
||||||
|
|
||||||
constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
|
|
||||||
if (key.length != 16) {
|
|
||||||
throw Error('incorrect key size');
|
|
||||||
}
|
|
||||||
if ((rounds & 1) != 0) {
|
|
||||||
throw Error('odd number of rounds specified');
|
|
||||||
}
|
|
||||||
|
|
||||||
const k = new DataView(key.buffer);
|
|
||||||
this.k0 = k.getUint32(0, false);
|
|
||||||
this.k1 = k.getUint32(4, false);
|
|
||||||
this.k2 = k.getUint32(8, false);
|
|
||||||
this.k3 = k.getUint32(12, false);
|
|
||||||
this.rounds = rounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt(dst: DataView, src: DataView) {
|
|
||||||
let v0 = src.getUint32(0, false);
|
|
||||||
let v1 = src.getUint32(4, false);
|
|
||||||
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < this.rounds / 2; i++) {
|
|
||||||
sum = sum + TeaCipher.delta;
|
|
||||||
v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
|
||||||
v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.setUint32(0, v0, false);
|
|
||||||
dst.setUint32(4, v1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(dst: DataView, src: DataView) {
|
|
||||||
let v0 = src.getUint32(0, false);
|
|
||||||
let v1 = src.getUint32(4, false);
|
|
||||||
|
|
||||||
let sum = (TeaCipher.delta * this.rounds) / 2;
|
|
||||||
for (let i = 0; i < this.rounds / 2; i++) {
|
|
||||||
v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
|
||||||
v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
|
||||||
sum -= TeaCipher.delta;
|
|
||||||
}
|
|
||||||
dst.setUint32(0, v0, false);
|
|
||||||
dst.setUint32(4, v1, false);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue