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

218 lines
7.7 KiB
Python

"""Weapon SDK plugin-loader client - pure-computation port of ``com.kuaishou.weapon.ks.u1``.
Fetches the dynamic weapon plugin manifest from the GDFP "plugin manager"
endpoint, decrypts ``antispamPluginManageRsp`` via :mod:`core.weapon_d0`, and
exposes the ``plugin`` map (each entry's ``wm`` is the dex download URL).
Reverse-engineered from ``u1.java`` / ``x0.java`` / ``h1.java`` / ``t.java`` /
``i.java`` / ``g.java`` under ``out/jadx/sources/com/kuaishou/weapon/ks/``.
Endpoint (``x0.f50698a`` + ``x0.f50700c``)::
https://gdfp.gifshow.com/rest/infra/gdfp/a/q
Request (``u1.a`` / ``u1.b`` / ``h1.b``)::
query = appkey=16&secretkey=<sk>&timestamp=<ts>&sign=md5(16+sk+ts)
body = {"data": d0.c(h1.b(ctx).toString())}
resp.result==1 -> d0.a(antispamPluginManageRsp) -> {status, policyId, plugin:{name:{wk,wan,wm,...}}}
The ``plugin`` map's p0 entry has ``wm`` = apk download URL (``b1.f50366i``) and
``apkMD5`` (``b1.f50367j``). The downloaded blob is then AES-decrypted (``l.b``)
before being loaded by ``a0`` (InMemoryDexClassLoader) - see
:func:`decrypt_plugin_blob`.
"""
from __future__ import annotations
import hashlib
import json
import time
from typing import Any, Callable, Dict, Optional, Tuple
from urllib.parse import quote
import requests
from .weapon_d0 import decrypt, encrypt
HOST = "https://gdfp.gifshow.com"
POLICY_PATH = "/rest/infra/gdfp/m/k" # x0.f50699b (p1: antispamSdkRsp)
PLUGINLOADER_PATH = "/rest/infra/gdfp/a/q" # x0.f50700c (u1: antispamPluginManageRsp)
POLICY_URL = HOST + POLICY_PATH
PLUGINLOADER_URL = HOST + PLUGINLOADER_PATH
APPKEY = "20001"
SECRETKEY = "117d05716732fb8835c5b32cdc6c5e9e" # hardcoded in WeaponSdkInitModule.smali:360,364
SDKVER = "7.2.1"
PIV = "v1" # ConsumeInfoUtils.f73701b (= h1.c "iv" and h1.b "piv")
PACKAGE_NAME = "com.kuaishou.nebula"
def _md5_hex(s: str) -> str:
return hashlib.md5(s.encode("utf-8")).hexdigest()
def build_plugin_query(ts: Optional[int] = None) -> str:
"""``u1.b`` / ``h1.d`` -> ``appkey=..&secretkey=..&timestamp=..&sign=md5(16+sk+ts)``."""
if ts is None:
ts = int(time.time())
sign = _md5_hex(APPKEY + SECRETKEY + str(ts))
return f"appkey={APPKEY}&secretkey={SECRETKEY}&timestamp={ts}&sign={sign}"
def build_h1b_json(profile) -> dict:
"""``h1.b(ctx)`` plaintext - u1 plugin-manager body descriptor."""
return {
"k": "",
"hp": PACKAGE_NAME, # t.e(ctx)
"hv": profile.app_version, # t.d(ctx)
"pver": "0.0.0", # z0 plc001_v_s default
"platform": 1,
"device_id": profile.did, # t.f(ctx)
"sdkver": SDKVER,
"piv": PIV, # ConsumeInfoUtils.f73701b
"sysver": f"ANDROID_{profile.android_release}", # t.f()
"mod": f"{profile.manufacturer}({profile.model})", # t.d()
}
def build_h1c_json(profile) -> dict:
"""``h1.c(ctx)`` plaintext - p1 policy body descriptor (``iv`` not ``piv``; no sysver/mod)."""
return {
"k": "",
"hp": PACKAGE_NAME,
"hv": profile.app_version,
"pver": "0.0.0",
"platform": 1,
"device_id": profile.did,
"sdkver": SDKVER,
"iv": PIV,
}
def build_plugin_cookie(profile) -> str:
"""``t.b()`` -> ``;``-joined device params (the ``Cookie`` header)."""
parts = [
("userId", ""),
("platform", ""),
("channel", ""),
("mod", quote(f"{profile.manufacturer}({profile.model})", safe="")),
("globalId", ""),
("sysver", quote(f"ANDROID_{profile.android_release}", safe="")),
("rdid", profile.rdid),
("did_tag", ""),
("cdid_tag", ""),
]
return ";".join(f"{k}={v}" for k, v in parts)
def _encrypt_body(h1_json: dict) -> str:
data = encrypt(json.dumps(h1_json, separators=(",", ":"), ensure_ascii=False))
return json.dumps({"data": data}, separators=(",", ":"))
def _gdfp_post(url, body, profile, *, post_func, timeout, include_cookie):
headers = {"Content-Type": "application/json"}
if include_cookie:
headers["Cookie"] = build_plugin_cookie(profile)
resp = post_func(url, data=body, headers=headers, timeout=timeout)
return resp
def _parse_gdfp_response(resp, rsp_field: str) -> Tuple[str, Optional[dict], Any]:
"""Common p1/u1 response parse: result==1 -> d0.a(<rsp_field>) -> JSON dict."""
text = getattr(resp, "text", str(resp))
try:
outer = json.loads(text)
except Exception:
return text, None, resp
if outer.get("result") != 1:
return text, None, resp
enc = outer.get(rsp_field, "")
if not enc:
return text, None, resp
try:
dec = decrypt(enc)
except Exception as exc: # noqa: BLE001
return f"<decrypt-failed: {exc.__class__.__name__}: {exc}>\nraw={enc[:200]}", None, resp
try:
inner = json.loads(dec)
except Exception:
return dec, None, resp
return dec, inner, resp
def fetch_policy(
profile,
*,
post_func: Optional[Callable] = None,
timeout: int = 30,
include_cookie: bool = True,
device_id: Optional[str] = None,
) -> Tuple[str, Optional[dict], Any]:
"""POST ``/rest/infra/gdfp/m/k`` (p1) -> d0.a(antispamSdkRsp) -> policy dict."""
if post_func is None:
post_func = requests.post
prof = _profile_with_device_id(profile, device_id) if device_id is not None else profile
ts = int(time.time())
url = f"{POLICY_URL}?{build_plugin_query(ts)}"
body = _encrypt_body(build_h1c_json(prof))
resp = _gdfp_post(url, body, prof, post_func=post_func, timeout=timeout, include_cookie=include_cookie)
return _parse_gdfp_response(resp, "antispamSdkRsp")
def fetch_plugin_manifest(
profile,
*,
post_func: Optional[Callable] = None,
timeout: int = 30,
include_cookie: bool = True,
device_id: Optional[str] = None,
) -> Tuple[str, Optional[dict], Any]:
"""POST ``/rest/infra/gdfp/a/q`` (u1) -> d0.a(antispamPluginManageRsp) -> plugin map.
Returns ``(decrypted_inner_str, plugin_map_or_inner_dict, response)``.
``plugin_map`` is ``None`` if result!=1 / no antispamPluginManageRsp / decrypt failed.
"""
if post_func is None:
post_func = requests.post
prof = _profile_with_device_id(profile, device_id) if device_id is not None else profile
ts = int(time.time())
url = f"{PLUGINLOADER_URL}?{build_plugin_query(ts)}"
body = _encrypt_body(build_h1b_json(prof))
resp = _gdfp_post(url, body, prof, post_func=post_func, timeout=timeout, include_cookie=include_cookie)
dec, inner, resp = _parse_gdfp_response(resp, "antispamPluginManageRsp")
plugin_map = inner.get("plugin") if isinstance(inner, dict) else None
return dec, plugin_map, resp
class _ShimProfile:
"""Lightweight profile override so we don't mutate the caller's object."""
def __init__(self, base, device_id):
self.__dict__.update(base.__dict__)
self.did = device_id
def _profile_with_device_id(profile, device_id):
return _ShimProfile(profile, device_id)
def find_p0_plugin(plugin_map: Optional[dict]) -> Optional[Tuple[str, dict]]:
"""Find the p0 plugin descriptor. Returns ``(plugin_name, descriptor_dict)``.
u1 selects p0 via ``b1.f50360c.contains("p0")`` (apkPackageName). Without
reversing the ``k`` getter mapping, we heuristically pick the entry whose
serialized descriptor (or key) mentions ``p0``.
"""
if not plugin_map:
return None
for name, desc in plugin_map.items():
if not isinstance(desc, dict):
continue
blob = json.dumps(desc, ensure_ascii=False) + name
if "p0" in blob:
return name, desc
return None