"""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=×tamp=&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=..×tamp=..&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}×tamp={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() -> 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"\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