276 lines
9.6 KiB
Python
276 lines
9.6 KiB
Python
"""Weapon p0 ``Engine.pr(99999, 0, ...)`` 的纯 Python VIMG base 生成器。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import struct
|
|
|
|
|
|
VIMG_PREFIX = "VIMG_"
|
|
_MASK32 = 0xFFFFFFFF
|
|
_XOR_BYTE = 0x55
|
|
|
|
_BLAKE2S_SIGMA = (
|
|
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
|
|
(14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3),
|
|
(11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4),
|
|
(7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8),
|
|
(9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13),
|
|
(2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9),
|
|
(12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11),
|
|
(13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10),
|
|
(6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5),
|
|
(10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0),
|
|
)
|
|
|
|
_H1_INITIAL_STATE = (
|
|
0xAA98186E,
|
|
0xF3CCD768,
|
|
0x99531AAE,
|
|
0x669781D3,
|
|
0x125FD5B4,
|
|
0x9883595E,
|
|
0x74F4CBCD,
|
|
0x98C26A78,
|
|
)
|
|
_H1_IV = (0xAB99184E, *_H1_INITIAL_STATE[1:])
|
|
_H2_ADD_BASE = bytes.fromhex("0c35ff3dcbfefb3f3efd6539fd39efcd")
|
|
_AI_XOR_MASK = bytes.fromhex("2dd345c0")
|
|
|
|
# 0x1a6b28 初始化、0x1a79f8 执行 20-round ChaCha block。这里保留 native
|
|
# 实际状态,不替换为标准的 "expand 32-byte k" 常量。
|
|
_CHACHA_STATE = (
|
|
0x1783595E,
|
|
0x8DC26A78,
|
|
0x2599184E,
|
|
0x729781D3,
|
|
0x2ADEF3F4,
|
|
0x9876EF16,
|
|
0x9ABED34F,
|
|
0x9103DE12,
|
|
0xA92157F6,
|
|
0xA9A24FF4,
|
|
0x9138D3FD,
|
|
0x2A2193F3,
|
|
1,
|
|
0x74F4CBCD,
|
|
0x98C26A78,
|
|
0xAB99184E,
|
|
)
|
|
|
|
|
|
def _rotate_left32(value: int, shift: int) -> int:
|
|
return ((value << shift) | (value >> (32 - shift))) & _MASK32
|
|
|
|
|
|
def _rotate_right32(value: int, shift: int) -> int:
|
|
return ((value >> shift) | (value << (32 - shift))) & _MASK32
|
|
|
|
|
|
def _quarter_round(words: list[int], a: int, b: int, c: int, d: int) -> None:
|
|
words[a] = (words[a] + words[b]) & _MASK32
|
|
words[d] = _rotate_left32(words[d] ^ words[a], 16)
|
|
words[c] = (words[c] + words[d]) & _MASK32
|
|
words[b] = _rotate_left32(words[b] ^ words[c], 12)
|
|
words[a] = (words[a] + words[b]) & _MASK32
|
|
words[d] = _rotate_left32(words[d] ^ words[a], 8)
|
|
words[c] = (words[c] + words[d]) & _MASK32
|
|
words[b] = _rotate_left32(words[b] ^ words[c], 7)
|
|
|
|
|
|
def _chacha_block(counter: int) -> bytes:
|
|
if not 0 <= counter <= _MASK32:
|
|
raise ValueError("VIMG ChaCha counter 超出 uint32")
|
|
|
|
initial = list(_CHACHA_STATE)
|
|
initial[12] = counter
|
|
working = initial.copy()
|
|
for _ in range(10):
|
|
_quarter_round(working, 0, 4, 8, 12)
|
|
_quarter_round(working, 1, 5, 9, 13)
|
|
_quarter_round(working, 2, 6, 10, 14)
|
|
_quarter_round(working, 3, 7, 11, 15)
|
|
_quarter_round(working, 0, 5, 10, 15)
|
|
_quarter_round(working, 1, 6, 11, 12)
|
|
_quarter_round(working, 2, 7, 8, 13)
|
|
_quarter_round(working, 3, 4, 9, 14)
|
|
|
|
return struct.pack(
|
|
"<16I",
|
|
*((value + original) & _MASK32 for value, original in zip(working, initial)),
|
|
)
|
|
|
|
|
|
def _java_modified_utf8(value: str) -> bytes:
|
|
"""复现 JNI ``GetStringUTFChars`` 对 Java String 的 modified UTF-8。"""
|
|
|
|
utf16 = value.encode("utf-16-be", errors="surrogatepass")
|
|
output = bytearray()
|
|
for offset in range(0, len(utf16), 2):
|
|
code_unit = int.from_bytes(utf16[offset : offset + 2], "big")
|
|
if 0x01 <= code_unit <= 0x7F:
|
|
output.append(code_unit)
|
|
elif code_unit <= 0x7FF:
|
|
output.extend((0xC0 | (code_unit >> 6), 0x80 | (code_unit & 0x3F)))
|
|
else:
|
|
output.extend(
|
|
(
|
|
0xE0 | (code_unit >> 12),
|
|
0x80 | ((code_unit >> 6) & 0x3F),
|
|
0x80 | (code_unit & 0x3F),
|
|
)
|
|
)
|
|
return bytes(output)
|
|
|
|
|
|
def _decode_java_modified_utf8(value: bytes) -> str:
|
|
code_units: list[int] = []
|
|
offset = 0
|
|
while offset < len(value):
|
|
first = value[offset]
|
|
if 0x01 <= first <= 0x7F:
|
|
code_units.append(first)
|
|
offset += 1
|
|
elif first & 0xE0 == 0xC0 and offset + 1 < len(value):
|
|
code_units.append(((first & 0x1F) << 6) | (value[offset + 1] & 0x3F))
|
|
offset += 2
|
|
elif first & 0xF0 == 0xE0 and offset + 2 < len(value):
|
|
code_units.append(
|
|
((first & 0x0F) << 12)
|
|
| ((value[offset + 1] & 0x3F) << 6)
|
|
| (value[offset + 2] & 0x3F)
|
|
)
|
|
offset += 3
|
|
else:
|
|
raise ValueError("VIMG payload 含非法 modified UTF-8")
|
|
utf16 = b"".join(code_unit.to_bytes(2, "big") for code_unit in code_units)
|
|
return utf16.decode("utf-16-be", errors="surrogatepass")
|
|
|
|
|
|
def _xor_chacha(data: bytes) -> bytes:
|
|
output = bytearray(len(data))
|
|
for block_index, offset in enumerate(range(0, len(data), 64), start=1):
|
|
key_stream = _chacha_block(block_index)
|
|
chunk = data[offset : offset + 64]
|
|
output[offset : offset + len(chunk)] = (
|
|
value ^ key_stream[index] for index, value in enumerate(chunk)
|
|
)
|
|
return bytes(output)
|
|
|
|
|
|
def generate_vimg_base(payload: str) -> str:
|
|
"""生成 ``Engine.pr(..., mode=0, payload)`` 的 ``VIMG_<base64>`` 部分。"""
|
|
|
|
payload_bytes = _java_modified_utf8(payload)
|
|
if len(payload_bytes) > 0xFFFF:
|
|
raise ValueError("VIMG payload 超过 native uint16 长度上限")
|
|
plain = b"\x2d\x3d\x00\x00\x7d\x01" + struct.pack("<H", len(payload_bytes)) + payload_bytes
|
|
native_buffer = bytes(value ^ _XOR_BYTE for value in plain)
|
|
encoded = base64.b64encode(_xor_chacha(native_buffer)).decode("ascii")
|
|
return VIMG_PREFIX + encoded
|
|
|
|
|
|
def _compress_h1(
|
|
state: list[int],
|
|
message: list[int],
|
|
counter: int,
|
|
is_last: bool,
|
|
) -> list[int]:
|
|
"""复现 0x1ab3d0 的自定义 IV BLAKE2s 压缩。"""
|
|
|
|
if len(message) != 16:
|
|
raise ValueError("H1 压缩块必须包含 16 个 uint32")
|
|
|
|
working = state.copy() + list(_H1_IV)
|
|
working[12] ^= counter & _MASK32
|
|
working[13] ^= (counter >> 32) & _MASK32
|
|
if is_last:
|
|
working[14] ^= _MASK32
|
|
|
|
def mix(a: int, b: int, c: int, d: int, x: int, y: int) -> None:
|
|
working[a] = (working[a] + working[b] + x) & _MASK32
|
|
working[d] = _rotate_right32(working[d] ^ working[a], 16)
|
|
working[c] = (working[c] + working[d]) & _MASK32
|
|
working[b] = _rotate_right32(working[b] ^ working[c], 12)
|
|
working[a] = (working[a] + working[b] + y) & _MASK32
|
|
working[d] = _rotate_right32(working[d] ^ working[a], 8)
|
|
working[c] = (working[c] + working[d]) & _MASK32
|
|
working[b] = _rotate_right32(working[b] ^ working[c], 7)
|
|
|
|
for schedule in _BLAKE2S_SIGMA:
|
|
mix(0, 4, 8, 12, message[schedule[0]], message[schedule[1]])
|
|
mix(1, 5, 9, 13, message[schedule[2]], message[schedule[3]])
|
|
mix(2, 6, 10, 14, message[schedule[4]], message[schedule[5]])
|
|
mix(3, 7, 11, 15, message[schedule[6]], message[schedule[7]])
|
|
mix(0, 5, 10, 15, message[schedule[8]], message[schedule[9]])
|
|
mix(1, 6, 11, 12, message[schedule[10]], message[schedule[11]])
|
|
mix(2, 7, 8, 13, message[schedule[12]], message[schedule[13]])
|
|
mix(3, 4, 9, 14, message[schedule[14]], message[schedule[15]])
|
|
|
|
return [
|
|
(state[index] ^ working[index] ^ working[index + 8]) & _MASK32
|
|
for index in range(8)
|
|
]
|
|
|
|
|
|
def _generate_h1_words(vimg_base: str) -> list[int]:
|
|
base_bytes = vimg_base.encode("ascii")
|
|
word_count = (len(base_bytes) + 3) // 4
|
|
padded = base_bytes.ljust(word_count * 4, b"\0")
|
|
words = list(struct.unpack(f"<{word_count}I", padded))
|
|
state = list(_H1_INITIAL_STATE)
|
|
|
|
# Native 每批读取最多 64 个字,再按索引模 16 折叠成 BLAKE2s 块。
|
|
for offset in range(0, word_count, 64):
|
|
source = words[offset : offset + 64]
|
|
folded = [0] * 16
|
|
for index, value in enumerate(source):
|
|
folded[index % 16] ^= value
|
|
counter = offset + len(source)
|
|
state = _compress_h1(
|
|
state,
|
|
folded,
|
|
counter,
|
|
is_last=counter == word_count,
|
|
)
|
|
return state
|
|
|
|
|
|
def _generate_ai_hex(vimg_base: str) -> str:
|
|
h1_text = "".join(f"{value:08x}" for value in _generate_h1_words(vimg_base)) + " "
|
|
h2 = bytes(
|
|
(((_H2_ADD_BASE[index] + 3) & 0xFF) ^ ord(h1_text[index]))
|
|
for index in range(16)
|
|
)
|
|
return bytes(
|
|
value ^ _AI_XOR_MASK[index % len(_AI_XOR_MASK)]
|
|
for index, value in enumerate(h2)
|
|
).hex()
|
|
|
|
|
|
def generate_passport_account_image(payload: str) -> str:
|
|
"""纯 Python 生成完整 ``passport_account_image``。"""
|
|
|
|
vimg_base = generate_vimg_base(payload)
|
|
return f"{vimg_base}$AI_{_generate_ai_hex(vimg_base)}"
|
|
|
|
|
|
def decode_passport_account_image_payload(value: str) -> str:
|
|
"""反解本地 ``Engine.pr(..., mode=0)`` 票据并返回原始 Java 字符串。"""
|
|
|
|
vimg_base = str(value).split("$AI_", 1)[0]
|
|
if not vimg_base.startswith(VIMG_PREFIX):
|
|
raise ValueError("passport_account_image 缺少 VIMG_ 前缀")
|
|
try:
|
|
cipher = base64.b64decode(vimg_base[len(VIMG_PREFIX) :], validate=True)
|
|
except (ValueError, base64.binascii.Error) as exc:
|
|
raise ValueError("passport_account_image Base64 非法") from exc
|
|
native_buffer = _xor_chacha(cipher)
|
|
plain = bytes(value ^ _XOR_BYTE for value in native_buffer)
|
|
if len(plain) < 8 or plain[:6] != b"\x2d\x3d\x00\x00\x7d\x01":
|
|
raise ValueError("passport_account_image VIMG 头非法")
|
|
payload_length = struct.unpack_from("<H", plain, 6)[0]
|
|
if len(plain) != payload_length + 8:
|
|
raise ValueError("passport_account_image payload 长度不匹配")
|
|
return _decode_java_modified_utf8(plain[8:])
|