"""PrivacyKit / WeaponHI 风控票据相关工具。 已确认静态链路: ``WeaponHI.dd(21)`` -> 读取内存缓存或 SharedPreferences ``wcfg["a_y_q_z"]`` ``WeaponHI.b(str)`` -> GZIP(str.getBytes()) -> ``MXSec.atlasEncrypt("privacykit", UUID, 0, gzipBytes)`` -> native ``10400`` -> Base64.NO_WRAP 注意:``weaponhi_vimg_upload`` 复现的是 ``WeaponHI.b`` 的另一条上传链, 不等同于登录票据。登录请求里的 ``VIMG_$AI_<32hex>`` 由 ``Engine.pr(99999, 0, ...)`` 在本地生成,纯 Python 实现在 ``weapon_vimg``。 """ from __future__ import annotations import base64 import gzip import hashlib from dataclasses import asdict, dataclass from io import BytesIO from pathlib import Path from typing import Any, Mapping from .enc_data import ( KWSG_10400_DEFAULT_CFG9, ZT_OUTER_CONFIGS, kwsg_10400_raw, load_kwsg_10400_tables, parse_inner_zt_header, zt_outer_unwrap, ) from .weapon_vimg import ( decode_passport_account_image_payload, generate_passport_account_image, ) PRIVACYKIT_PRODUCT = "privacykit" PRIVACYKIT_SDK_ID = "7e46b28a-8c93-4940-8238-4c60e64e3c81" PASSPORT_ACCOUNT_IMAGE_PREFIX = "VIMG_" PASSPORT_ACCOUNT_IMAGE_AI_MARKER = "$AI_" PASSPORT_WCFG_KEY = "a_y_q_z" WEAPONHI_IMG_INITIAL = "R_I_N_I" @dataclass(frozen=True) class PassportImageInfo: """``passport_account_image`` 的可诊断摘要。""" ok: bool has_vimg_prefix: bool has_ai_hash: bool base64_len: int raw_len: int raw_head_hex: str format_kind: str sdk_id: str = "" ai_hash: str = "" ai_matches_md5_raw: bool = False ai_matches_md5_base64: bool = False ai_matches_local: bool = False inner_magic_hex: str = "" inner_header_size: int = 0 inner_payload_len: int = 0 error: str = "" def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(frozen=True) class WeaponHiDd21Result: """``WeaponHI.dd(21)`` 的纯 Python 状态机结果。 静态 smali 对齐: - ``WeaponHI.img`` 初值是 ``R_I_N_I``。 - 若 ``img.startsWith("VIMG_")``,``dd`` 直接返回内存缓存。 - 否则读取 ``wcfg["a_y_q_z"]``,默认值为旧 ``img``,并写回 ``img``。 ``wcfg`` 只承担本地持久化;票据内容可由 ``weapon_mf`` 和 ``weapon_vimg`` 现场纯算,不依赖服务端回写。 """ value: str source: str mtype: int cache_before: str cache_after: str wcfg_key: str has_wcfg_value: bool is_final_ticket: bool is_upload_vimg: bool diagnosis: dict[str, Any] @property def ok(self) -> bool: return bool(self.value and self.value.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX)) def to_dict(self) -> dict[str, Any]: out = asdict(self) out["ok"] = self.ok return out def java_gzip(data: bytes) -> bytes: """生成接近 Java ``GZIPOutputStream`` 的 gzip bytes。 Java 样本头固定为 ``1f8b08000000000000ff``: - mtime = 0 - XFL = 0(默认压缩级别) - OS = 255 """ buf = BytesIO() with gzip.GzipFile(fileobj=buf, mode="wb", compresslevel=6, mtime=0) as gz: gz.write(bytes(data)) out = buf.getvalue() if len(out) >= 10 and out[:3] == b"\x1f\x8b\x08": out = out[:8] + b"\x00\xff" + out[10:] return out def _tables_or_load( t1: bytes | None, t2: bytes | None, *, t1_path: str | Path = "bin/kwsg_10400_T1.bin", t2_path: str | Path = "bin/kwsg_10400_T2.bin", ) -> tuple[bytes, bytes]: if t1 is None and t2 is None: return load_kwsg_10400_tables(t1_path, t2_path) if t1 is None or t2 is None: raise ValueError("t1 and t2 must be provided together") return t1, t2 def privacykit_atlas_encrypt_raw( payload: bytes, *, sdk_id: str = PRIVACYKIT_SDK_ID, epoch_seconds: int | None = None, cfg9: bytes = KWSG_10400_DEFAULT_CFG9, t1: bytes | None = None, t2: bytes | None = None, ) -> bytes: """复现 ``atlasEncrypt("privacykit", sdk_id, 0, payload)`` 的 10400 raw。""" if not isinstance(payload, (bytes, bytearray)): raise TypeError("payload must be bytes") if sdk_id not in ZT_OUTER_CONFIGS: raise ValueError(f"unknown privacykit sdk_id: {sdk_id}") if len(cfg9) != 9: raise ValueError("cfg9 must be exactly 9 bytes") table1, table2 = _tables_or_load(t1, t2) return kwsg_10400_raw( bytes(payload), sdk_id, table1, table2, epoch_seconds=epoch_seconds, cfg9=cfg9, ) def weaponhi_b( value: str, *, sdk_id: str = PRIVACYKIT_SDK_ID, epoch_seconds: int | None = None, cfg9: bytes = KWSG_10400_DEFAULT_CFG9, t1: bytes | None = None, t2: bytes | None = None, ) -> str: """复现 ``WeaponHI.b(str)``:gzip -> privacykit 10400 -> Base64.NO_WRAP。""" payload = java_gzip(str(value).encode("utf-8")) raw = privacykit_atlas_encrypt_raw( payload, sdk_id=sdk_id, epoch_seconds=epoch_seconds, cfg9=cfg9, t1=t1, t2=t2, ) return base64.b64encode(raw).decode("ascii") def weaponhi_vimg_upload(value: str, **kwargs: Any) -> str: """构造上传态 ``VIMG_`` 值。 该值用于对齐 ``WeaponHI.b`` 上游加密积木,不等同于登录请求中的 ``Engine.pr`` 票据。 """ return PASSPORT_ACCOUNT_IMAGE_PREFIX + weaponhi_b(value, **kwargs) def _mapping_get(mapping: Mapping[str, Any] | dict[str, Any] | None, key: str, default: str) -> str: if mapping is None: return default try: value = mapping.get(key, default) # type: ignore[attr-defined] except AttributeError: return default if value is None: return default return str(value) def weaponhi_dd21_from_wcfg( wcfg: Mapping[str, Any] | dict[str, Any] | None, *, img_cache: str = WEAPONHI_IMG_INITIAL, mtype: int = 21, ) -> WeaponHiDd21Result: """按 smali 复现 ``WeaponHI.dd(21)`` 的读取/缓存语义。 参数 ``wcfg`` 是已解析的 SharedPreferences/wcfg 字典;只读取 ``a_y_q_z``。如果当前 ``img_cache`` 已经是 ``VIMG_``,则完全复用缓存, 不再读取 wcfg。 """ cache_before = str(img_cache or "") if cache_before.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX): value = cache_before source = "img_cache" has_wcfg_value = bool(_mapping_get(wcfg, PASSPORT_WCFG_KEY, "")) else: wcfg_value = _mapping_get(wcfg, PASSPORT_WCFG_KEY, cache_before) value = wcfg_value source = f"wcfg.{PASSPORT_WCFG_KEY}" if wcfg_value != cache_before else "default_img_cache" has_wcfg_value = wcfg_value != cache_before diagnosis = diagnose_passport_account_image(value) if value else {} return WeaponHiDd21Result( value=value, source=source, mtype=int(mtype), cache_before=cache_before, cache_after=value, wcfg_key=PASSPORT_WCFG_KEY, has_wcfg_value=has_wcfg_value, is_final_ticket=bool( value.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX) and PASSPORT_ACCOUNT_IMAGE_AI_MARKER in value and diagnosis.get("ok") ), is_upload_vimg=bool( value.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX) and PASSPORT_ACCOUNT_IMAGE_AI_MARKER not in value and diagnosis.get("format_kind") == "zt_outer" ), diagnosis=diagnosis, ) def _split_passport_value(value: str) -> tuple[str, str]: text = str(value or "").strip() if text.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX): text = text[len(PASSPORT_ACCOUNT_IMAGE_PREFIX):] if PASSPORT_ACCOUNT_IMAGE_AI_MARKER in text: body, ai_hash = text.split(PASSPORT_ACCOUNT_IMAGE_AI_MARKER, 1) return body, ai_hash return text, "" def _known_outer_sdk_id(raw: bytes) -> str: for sdk_id, cfg in ZT_OUTER_CONFIGS.items(): if raw.startswith(cfg["head8"]): return sdk_id return "" def parse_passport_account_image(value: str) -> PassportImageInfo: """解析 ``passport_account_image``,返回形态和哈希诊断。 已知两类形态: - ``zt_outer``:本地 ``WeaponHI.b`` 生成的 ``5a54...`` 外层 ZT 包。 - ``weapon_pr``:本地 ``Engine.pr`` 生成的 ``VIMG_...$AI_...`` 票据。 """ original = str(value or "").strip() if not original: raise ValueError("passport_account_image is empty") has_vimg = original.startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX) body_b64, ai_hash = _split_passport_value(original) raw = base64.b64decode(body_b64, validate=True) raw_head_hex = raw[:16].hex() md5_raw = hashlib.md5(raw).hexdigest() md5_b64 = hashlib.md5(body_b64.encode("ascii")).hexdigest() sdk_id = _known_outer_sdk_id(raw) format_kind = "unknown" inner_magic_hex = "" inner_header_size = 0 inner_payload_len = 0 ai_matches_local = False if has_vimg and ai_hash: format_kind = "weapon_pr" try: payload = decode_passport_account_image_payload(original) inner_magic_hex = "2d3d00007d01" inner_header_size = 8 inner_payload_len = len(payload.encode("utf-8")) ai_matches_local = generate_passport_account_image(payload) == original except Exception: pass elif sdk_id: format_kind = "zt_outer" try: inner = zt_outer_unwrap(raw, ZT_OUTER_CONFIGS[sdk_id]["xor_key"]) parsed = parse_inner_zt_header(inner) inner_magic_hex = parsed["magic"].hex() inner_header_size = int(parsed["header_size"]) inner_payload_len = int(parsed["payload_len"]) except Exception: # 保留 zt_outer 识别结果;inner 解析失败时诊断字段留空。 pass elif raw.startswith(bytes.fromhex("dec0adde")): format_kind = "inner_zt" try: parsed = parse_inner_zt_header(raw) inner_magic_hex = parsed["magic"].hex() inner_header_size = int(parsed["header_size"]) inner_payload_len = int(parsed["payload_len"]) except Exception: pass return PassportImageInfo( ok=True, has_vimg_prefix=has_vimg, has_ai_hash=bool(ai_hash), base64_len=len(body_b64), raw_len=len(raw), raw_head_hex=raw_head_hex, format_kind=format_kind, sdk_id=sdk_id, ai_hash=ai_hash, ai_matches_md5_raw=bool(ai_hash) and ai_hash.lower() == md5_raw, ai_matches_md5_base64=bool(ai_hash) and ai_hash.lower() == md5_b64, ai_matches_local=ai_matches_local, inner_magic_hex=inner_magic_hex, inner_header_size=inner_header_size, inner_payload_len=inner_payload_len, ) def diagnose_passport_account_image(value: str) -> dict[str, Any]: """JSON-friendly 诊断;解析失败也返回结构化错误。""" try: return parse_passport_account_image(value).to_dict() except Exception as exc: # noqa: BLE001 return PassportImageInfo( ok=False, has_vimg_prefix=str(value or "").startswith(PASSPORT_ACCOUNT_IMAGE_PREFIX), has_ai_hash=PASSPORT_ACCOUNT_IMAGE_AI_MARKER in str(value or ""), base64_len=0, raw_len=0, raw_head_hex="", format_kind="invalid", error=f"{exc.__class__.__name__}: {exc}", ).to_dict() __all__ = [ "PASSPORT_ACCOUNT_IMAGE_AI_MARKER", "PASSPORT_ACCOUNT_IMAGE_PREFIX", "PASSPORT_WCFG_KEY", "PRIVACYKIT_PRODUCT", "PRIVACYKIT_SDK_ID", "PassportImageInfo", "WEAPONHI_IMG_INITIAL", "WeaponHiDd21Result", "diagnose_passport_account_image", "java_gzip", "parse_passport_account_image", "privacykit_atlas_encrypt_raw", "weaponhi_dd21_from_wcfg", "weaponhi_b", "weaponhi_vimg_upload", ]