mirror of
				https://git.unlock-music.dev/um/web.git
				synced 2025-11-04 23:13:30 +08:00 
			
		
		
		
	feat(QMCv2): add rc4 cipher
(cherry picked from commit 6b5b4d3bf5f6285e908808d48dee4e2e4ae8c3a2)
This commit is contained in:
		
							parent
							
								
									23b096512e
								
							
						
					
					
						commit
						910b00529e
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user