diff --git a/src/decrypt/qmc_cipher.test.ts b/src/decrypt/qmc_cipher.test.ts index a4d5513..a54d0f8 100644 --- a/src/decrypt/qmc_cipher.test.ts +++ b/src/decrypt/qmc_cipher.test.ts @@ -1,4 +1,4 @@ -import {QmcMapCipher, QmcStaticCipher} from "@/decrypt/qmc_cipher"; +import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher"; import fs from 'fs' test("static cipher [0x7ff8,0x8000) ", () => { @@ -27,17 +27,6 @@ test("static cipher [0,0x10) ", () => { 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", () => { const expected = new Uint8Array([ @@ -53,10 +42,22 @@ test("map cipher: get mask", () => { 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} = loadTestDataMapCipher(name) + const {key, clearText, cipherText} = loadTestDataCipher(name) const c = new QmcMapCipher(key) c.decrypt(cipherText, 0) @@ -64,3 +65,51 @@ test("map cipher: real file", async () => { 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)) + } +}) diff --git a/src/decrypt/qmc_cipher.ts b/src/decrypt/qmc_cipher.ts index 532625a..1508f5e 100644 --- a/src/decrypt/qmc_cipher.ts +++ b/src/decrypt/qmc_cipher.ts @@ -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 + } +}