218 lines
7.7 KiB
Python
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>×tamp=<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=..×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(<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
|