ksjsb/core/weapon_vimg.py
2026-07-30 20:25:56 +08:00

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:])