feat(QMCv2): add rc4 cipher
(cherry picked from commit 6b5b4d3bf5f6285e908808d48dee4e2e4ae8c3a2)
This commit is contained in:
parent
4703667a44
commit
76c3887eec
@ -1,4 +1,4 @@
|
|||||||
import {QmcMapCipher, QmcStaticCipher} from "@/decrypt/qmc_cipher";
|
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher";
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
test("static cipher [0x7ff8,0x8000) ", () => {
|
test("static cipher [0x7ff8,0x8000) ", () => {
|
||||||
@ -27,17 +27,6 @@ test("static cipher [0,0x10) ", () => {
|
|||||||
expect(buf).toStrictEqual(expected)
|
expect(buf).toStrictEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
function loadTestDataMapCipher(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: get mask", () => {
|
test("map cipher: get mask", () => {
|
||||||
const expected = new Uint8Array([
|
const expected = new Uint8Array([
|
||||||
@ -53,10 +42,22 @@ test("map cipher: get mask", () => {
|
|||||||
expect(buf).toStrictEqual(expected)
|
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 () => {
|
test("map cipher: real file", async () => {
|
||||||
const cases = ["mflac_map", "mgg_map"]
|
const cases = ["mflac_map", "mgg_map"]
|
||||||
for (const name of cases) {
|
for (const name of cases) {
|
||||||
const {key, clearText, cipherText} = loadTestDataMapCipher(name)
|
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||||
const c = new QmcMapCipher(key)
|
const c = new QmcMapCipher(key)
|
||||||
|
|
||||||
c.decrypt(cipherText, 0)
|
c.decrypt(cipherText, 0)
|
||||||
@ -64,3 +65,51 @@ test("map cipher: real file", async () => {
|
|||||||
expect(cipherText).toStrictEqual(clearText)
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -83,3 +83,120 @@ export class QmcMapCipher implements StreamCipher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FIRST_SEGMENT_SIZE = 0x80;
|
||||||
|
const SEGMENT_SIZE = 5120
|
||||||
|
|
||||||
|
export class QmcRC4Cipher implements StreamCipher {
|
||||||
|
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 < FIRST_SEGMENT_SIZE) {
|
||||||
|
const len_segment = Math.min(buf.length, FIRST_SEGMENT_SIZE - offset);
|
||||||
|
this.encFirstSegment(buf.subarray(0, len_segment), offset);
|
||||||
|
if (postProcess(len_segment)) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// align segment
|
||||||
|
if (offset % SEGMENT_SIZE != 0) {
|
||||||
|
const len_segment = Math.min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), toProcess);
|
||||||
|
this.encASegment(buf.subarray(processed, processed + len_segment), offset);
|
||||||
|
if (postProcess(len_segment)) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch process segments
|
||||||
|
while (toProcess > SEGMENT_SIZE) {
|
||||||
|
this.encASegment(buf.subarray(processed, processed + SEGMENT_SIZE), offset);
|
||||||
|
postProcess(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.getSegmentSkip(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 % SEGMENT_SIZE) + this.getSegmentSkip(offset / 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 getSegmentSkip(id: number): number {
|
||||||
|
const seed = this.key[id % this.N]
|
||||||
|
const idx = (this.hash / ((id + 1) * seed) * 100.0) | 0;
|
||||||
|
return idx % this.N
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user