| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- """Self-contained cocos2d-x XXTEA Lua decryptor.
- File format (per cocos2d-x xxtea.c + cq2-00 docs):
- [8-byte sign "4meJnPyl"] [XXTEA ciphertext of zlib-compressed lua source]
- Key: "34UFXMgtkz" padded to 16 bytes with NULs.
- Decrypt flow (mirrors cocos exactly):
- 1. ciphertext bytes -> uint32 LE array (no length append)
- 2. standard XXTEA decrypt (delta=0x9E3779B9, rounds=6+52//n)
- 3. last uint32 = real plaintext length -> truncate
- 4. zlib decompress
- Usage:
- uv run python scripts/decrypt_client_lua.py # decrypts key proxies
- uv run python scripts/decrypt_client_lua.py ALL # decrypt whole stab tree
- """
- import struct
- import sys
- import zlib
- from pathlib import Path
- SIGN = b"4meJnPyl"
- KEY = b"34UFXMgtkz".ljust(16, b"\0")
- DELTA = 0x9E3779B9
- STAB = Path(r"D:\ProgramFiles\996box\GameBoxDownload\1\cq\cache_206923\mod_fgcq\stab\scripts")
- OUT = Path("lua-dec2")
- def xxtea_decrypt(v: list[int], key: list[int]) -> list[int]:
- n = len(v)
- if n < 2:
- return v # cocos: n<1 returns; n==1 unchanged by loop below effectively
- rounds = 6 + 52 // n
- total = (rounds * DELTA) & 0xFFFFFFFF
- y = v[0]
- while total != 0:
- e = (total >> 2) & 3
- for p in range(n - 1, 0, -1):
- z = v[p - 1]
- mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[(p & 3) ^ e] ^ z))
- mx &= 0xFFFFFFFF
- y = v[p] = (v[p] - mx) & 0xFFFFFFFF
- z = v[n - 1]
- mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[0 ^ e] ^ z))
- mx &= 0xFFFFFFFF
- y = v[0] = (v[0] - mx) & 0xFFFFFFFF
- total = (total - DELTA) & 0xFFFFFFFF
- return v
- def bytes_to_u32(data: bytes, include_length: bool) -> list[int]:
- # cocos xxtea_to_uint_array: pad tail bytes into last u32 by OR (no truncation)
- n = (len(data) >> 2) + (0 if (len(data) & 3) == 0 else 1)
- v = [0] * n
- for i in range(len(data)):
- v[i >> 2] |= data[i] << ((i & 3) << 3)
- if include_length:
- v.append(len(data))
- return v
- def u32_to_bytes(v: list[int]) -> bytes:
- return struct.pack(f"<{len(v)}I", *v)
- def cocos_decrypt(raw: bytes) -> bytes:
- if raw[:8] != SIGN:
- raise ValueError(f"bad sign {raw[:8]!r}")
- cipher = raw[8:]
- v = bytes_to_u32(cipher, include_length=False)
- key = bytes_to_u32(KEY, include_length=False)
- v = xxtea_decrypt(v, key)
- real_len = v[-1]
- if real_len <= 0 or real_len > (len(v) - 1) * 4:
- raise ValueError(f"implausible length {real_len}")
- plain = u32_to_bytes(v[:-1])[:real_len]
- # This game's scripts are stored as plain Lua text (no zlib layer).
- # But tolerate both: if it starts with zlib magic, decompress.
- if plain[:2] == b"\x78\x9c" or plain[:2] == b"\x78\x01" or plain[:2] == b"\x78\xda":
- plain = zlib.decompress(plain)
- return plain
- def decrypt_file(src: Path, dst: Path) -> bool:
- try:
- plain = cocos_decrypt(src.read_bytes())
- dst.parent.mkdir(parents=True, exist_ok=True)
- dst.write_bytes(plain)
- return True
- except Exception as e:
- print(f" FAIL {src.name}: {type(e).__name__}: {e}")
- return False
- def main() -> int:
- mode = sys.argv[1] if len(sys.argv) > 1 else "KEY"
- if mode.upper() == "ALL":
- files = list(STAB.rglob("*.lua"))
- out_root = OUT / "stab"
- ok = 0
- for i, f in enumerate(files):
- rel = f.relative_to(STAB)
- if decrypt_file(f, out_root / rel):
- ok += 1
- if i % 200 == 0:
- print(f" {i}/{len(files)}...")
- print(f"decrypted {ok}/{len(files)} -> {out_root}")
- else:
- targets = [
- "game/proxy/remote/AuthProxy.lua",
- "game/proxy/remote/LoginOtpPassWordProxy.lua",
- "game/proxy/remote/LoginProxy.lua",
- "game/proxy/remote/RemoteProxy.lua",
- "network/networkUtil.lua",
- "game/proxy/GameEnvironmentProxy.lua",
- ]
- for t in targets:
- src = STAB / t
- if not src.exists():
- print(f" missing: {t}")
- continue
- dst = OUT / t
- if decrypt_file(src, dst):
- print(f" OK: {t} -> {dst} ({dst.stat().st_size}B)")
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|