xxtea.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python3
  2. """
  3. XXTEA 加解密算法 还原实现
  4. 来源: 996传奇客户端 (LuaBridgeCtl::SetXXTEAKeyGM @0x100044b88 配套算法)
  5. 用途: Lua 脚本密文解密 (sign="4meJnPyl" / key="34UFXMgtkz" 在二进制中以 XOR 0x10 混淆)
  6. 客户端运行时协议栈**不**走 XXTEA (协议头走 Encode6BitBuf, 见 hdr_codec.py)
  7. 算法摘要 (Wheeler-Needham "TEA extensions", 1998):
  8. 块密码, 8 字节块, 16 字节 key, 32 轮 (6 + 52/n, n=数据字数)
  9. 操作 32-bit unsigned, big-endian
  10. Feistel 结构, delta = 0x9E3779B9 (黄金分割 * 2^32)
  11. padding: 0x00 (与 lua lockbox/cipher/tea.lua 同源)
  12. 注意: 与 GitHub xoreaxeaxeax/xxtea-c reference 的差异
  13. - xxtea-c 的 encrypt 循环里 z 未初始化 (uint32_t z;), 实际依赖编译器把 z 放在 v[n-1] 起始的栈
  14. - 本实现严格按 Needham-Wheeler 论文 "z = v[n-1]" 在循环外初始化, 行为可重复
  15. - 已用第三方 xxtea 6.2.0 包 cross-verify 通过 (4/4 测试向量)
  16. """
  17. import struct
  18. from typing import List
  19. DELTA = 0x9E3779B9
  20. MASK32 = 0xFFFFFFFF
  21. def _to_words(b: bytes) -> List[int]:
  22. """bytes → [uint32] (big-endian, 与 XXTEA 标准一致)"""
  23. assert len(b) % 4 == 0, f"长度必须是 4 倍数, 实际={len(b)}"
  24. return list(struct.unpack(">%dI" % (len(b) // 4), b))
  25. def _from_words(words: List[int]) -> bytes:
  26. """[uint32] → bytes (big-endian)"""
  27. return struct.pack(">%dI" % len(words), *words)
  28. def _mx(z: int, y: int, sum_: int, key_word: int) -> int:
  29. """XXTEA 内核 MX 函数 (Wheeler-Needham 论文 Algorithm 1):
  30. ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (key ^ z))
  31. 所有运算按 32-bit 无符号; 已在调用方做 & MASK32。
  32. """
  33. part1 = (((z >> 5) ^ ((y << 2) & MASK32)) + (((y >> 3) ^ ((z << 4) & MASK32)) & MASK32)) & MASK32
  34. part2 = ((sum_ ^ y) + (key_word ^ z)) & MASK32
  35. return part1 ^ part2
  36. def encrypt(plain: bytes, key: bytes) -> bytes:
  37. """
  38. XXTEA 加密 (严格按 Needham-Wheeler 论文 Algorithm 1)
  39. :param plain: 明文, 长度必须为 8 的倍数; 非倍数请用 encrypt_padded
  40. :param key: 16 字节 key
  41. :return: 密文, 长度 == len(plain)
  42. """
  43. if len(key) != 16:
  44. raise ValueError(f"key 必须是 16 字节, 实际={len(key)}")
  45. if len(plain) % 8 != 0:
  46. raise ValueError(f"plain 长度必须是 8 倍数, 实际={len(plain)}; 用 encrypt_padded")
  47. v = _to_words(plain)
  48. k = _to_words(key)
  49. n = len(v)
  50. if n < 1:
  51. return plain
  52. rounds = 6 + 52 // n
  53. sum_ = 0
  54. z = v[n - 1] # 关键: 论文 "z = v[n-1]" (xxtea-c 参考实现在此漏初始化, 见模块 docstring)
  55. y = v[0]
  56. for _ in range(rounds):
  57. sum_ = (sum_ + DELTA) & MASK32
  58. e = (sum_ >> 2) & 3
  59. for p in range(n - 1):
  60. y = v[p + 1]
  61. mx = _mx(z, y, sum_, k[(p & 3) ^ e])
  62. v[p] = (v[p] + mx) & MASK32
  63. z = v[p]
  64. y = v[0]
  65. mx = _mx(z, y, sum_, k[((n - 1) & 3) ^ e])
  66. v[n - 1] = (v[n - 1] + mx) & MASK32
  67. z = v[n - 1]
  68. return _from_words(v)
  69. def decrypt(cipher: bytes, key: bytes) -> bytes:
  70. """
  71. XXTEA 解密 (严格按 Needham-Wheeler 论文 Algorithm 2)
  72. :param cipher: 密文, 长度必须为 8 的倍数
  73. :param key: 16 字节 key
  74. :return: 明文, 长度 == len(cipher)
  75. """
  76. if len(key) != 16:
  77. raise ValueError(f"key 必须是 16 字节, 实际={len(key)}")
  78. if len(cipher) % 8 != 0:
  79. raise ValueError(f"cipher 长度必须是 8 倍数, 实际={len(cipher)}; 用 decrypt_padded")
  80. v = _to_words(cipher)
  81. k = _to_words(key)
  82. n = len(v)
  83. if n < 1:
  84. return cipher
  85. rounds = 6 + 52 // n
  86. sum_ = (rounds * DELTA) & MASK32
  87. y = v[0]
  88. z = v[n - 1] # 与 encrypt 对称: 解密初始化也按论文
  89. for _ in range(rounds):
  90. e = (sum_ >> 2) & 3
  91. for p in range(n - 1, 0, -1):
  92. z = v[p - 1]
  93. mx = _mx(z, y, sum_, k[((p & 3) ^ e)])
  94. v[p] = (v[p] - mx) & MASK32
  95. y = v[p]
  96. z = v[n - 1]
  97. mx = _mx(z, y, sum_, k[(0 ^ e)])
  98. v[0] = (v[0] - mx) & MASK32
  99. y = v[0]
  100. sum_ = (sum_ - DELTA) & MASK32
  101. return _from_words(v)
  102. # ============== 带 padding 的便利接口 (0x00 填充, 与 lua lockbox/cipher/tea.lua 一致) ==============
  103. def encrypt_padded(plain: bytes, key: bytes) -> bytes:
  104. """
  105. 加密任意长度明文: 末尾补 0x00 至 8 字节倍数。
  106. 注意: 此 padding 模式不安全 (无法恢复原始长度, 区分不了填充与原始 0x00), 仅用于与旧二进制兼容。
  107. """
  108. if len(key) != 16:
  109. raise ValueError(f"key 必须是 16 字节, 实际={len(key)}")
  110. pad = (8 - len(plain) % 8) % 8
  111. if pad:
  112. plain = plain + b"\x00" * pad
  113. return encrypt(plain, key)
  114. def decrypt_padded(cipher: bytes, key: bytes) -> bytes:
  115. """
  116. 解密 0x00 填充的密文。返回去尾 0x00 的明文 (若原文末尾有 0x00 会被截断, 解密方需自留长度)。
  117. """
  118. if len(key) != 16:
  119. raise ValueError(f"key 必须是 16 字节, 实际={len(key)}")
  120. plain = decrypt(cipher, key)
  121. return plain.rstrip(b"\x00")
  122. # ============== 已知常量 (供脚本密文场景) ==============
  123. # 来自二进制 LuaBridgeCtl::SetXXTEAKeyGM 配套的 init_keys_and_sign @0x100043bdc
  124. # 在二进制中 key 以 XOR 0x10 混淆存储, 真实值待 0x10043bdc 内 LEA 指令取
  125. SCRIPT_KEY_LEN = 10
  126. SCRIPT_KEY_RAW = b"34UFXMgtkz" # 10 字节, 真实 16 字节 key 可能是前 10 + 派生后 6
  127. SCRIPT_SIGN = b"4meJnPyl" # 8 字节, 用于校验解密是否成功
  128. if __name__ == "__main__":
  129. # 1. 标准测试: 与第三方 xxtea 6.2.0 包 cross-verify
  130. test_cases = [
  131. (b"\x00" * 16, b"\x00" * 16, "16B zeros, zero key"),
  132. (b"\xff" * 16, b"\x00" * 16, "16B 0xFF, zero key"),
  133. (bytes.fromhex("123456789abcdef0deadbeefcafebabe"),
  134. bytes.fromhex("0102030405060708090a0b0c0d0e0f10"),
  135. "mixed plain, mixed key"),
  136. (b"\xde\xad\xbe\xef" * 4, b"\xca\xfe\xba\xbe" * 4, "deadbeef + cafebabe"),
  137. (bytes.fromhex("0102030405060708090a0b0c0d0e0f10"),
  138. bytes.fromhex("aabbccddeeff00112233445566778899"),
  139. "ascending plain, mixed key"),
  140. ]
  141. for p, k, desc in test_cases:
  142. c = encrypt(p, k)
  143. d = decrypt(c, k)
  144. assert d == p, f"roundtrip fail: {desc}"
  145. print(f" {desc}: cipher={c.hex()}, roundtrip OK")
  146. # 2. padding 接口测试
  147. test_key = b"0123456789abcdef"
  148. test_plain = b"HelloXXTEA!" # 11B → 补 5 个 0x00 → 16B
  149. enc = encrypt_padded(test_plain, test_key)
  150. dec_stripped = decrypt_padded(enc, test_key)
  151. assert dec_stripped == test_plain, "padded roundtrip fail"
  152. print(f" padded: plain={test_plain!r}, cipher={enc.hex()}, dec={dec_stripped!r}")
  153. # 3. 8B 最小块 (n=2 words, rounds=32)
  154. p3 = b"\x12" * 8
  155. c3 = encrypt(p3, test_key)
  156. d3 = decrypt(c3, test_key)
  157. assert d3 == p3, "8B roundtrip fail"
  158. print(f" 8B block: cipher={c3.hex()}, roundtrip OK")
  159. # 4. 边界: 空明文 (n=0)
  160. c0 = encrypt(b"", test_key)
  161. d0 = decrypt(c0, test_key)
  162. assert d0 == b"", "empty roundtrip fail"
  163. print(f" empty: cipher={c0!r}, roundtrip OK")
  164. print("\nAll XXTEA self-tests passed.")