[Codex全程] KWAI REST / sig / NStokensig / sig3

Codex全程自动逆向Kwai sig, sig3

[Codex全程] KWAI REST / sig / NStokensig / sig3

全程Codex, 我只测试了一下感觉大概能用, 包括文章也是, 只有这里是我自己写的, 他没解决DFP

我靠... 逆向你也可以失业了...


0x00 前言:这次到底要解决什么

一开始目标不是“随便发一个 HTTP 请求”。快手这种 App 的 REST 请求至少有四层东西会互相影响:

  1. Retrofit/Jadx 里能看到的接口路径和字段。
  2. sig__NStokensig 这种 Java 参数层签名。
  3. __NS_sig3 这种从 Java 进入 KSecurity/native 的安全签名。
  4. 设备态、DFP、DID/EGID、Azeroth、HAR 登录态和 Cronet 抓包环境。

如果只看 HAR,会得到一堆字段,但不知道字段怎么生成;如果只看 Java,会看到 KSecurity.atlasSign(),但不知道 native 里 command id 怎么走;如果只看 IDA,会看到混淆 C 伪代码,但不知道业务请求把什么字符串送进去。

所以我最后采用的路线是:

flowchart TD
  A["HAR / Retrofit annotation"] --> B["ParamsUtils 合并参数"]
  B --> C["KwaiParams 计算 sig / token sig"]
  C --> D["path + sig -> WebUtils.a"]
  D --> E["KSecurity.atlasSign"]
  E --> F["bridge / JNI / libkwsgmain"]
  F --> G["IDA command 10405 / 10417 / 10418"]
  G --> H["Python algorithms.py"]
  H --> I["client.py 封装 REST / 登录 / HAR / 私信"]
  I --> J["kws_client.py CLI + tests"]

下面从最外层请求开始往里剥。


0x01 先从 Retrofit 参数处理下手:sig__NStokensig 是怎么塞进去的

抓包里能看到 sig,但 HAR 本身只能证明“请求里有这个字段”,不能证明签名输入。Jadx 里先搜 __NStokensig,最短路径直接落到 ParamsUtils.java。这段代码很短,但价值很高:它告诉我们公共参数、业务参数、auth 参数怎么合并,sig 放在哪里,client_salt 什么时候参与。

Retrofit 参数合并证据:ParamsUtils.java

  • 文件:sources/com/yxcorp/retrofit/utils/ParamsUtils.java
  • 行数:55
  • 字节:1761
  • SHA256:c33956be7f272026a6d5bcfa75b4550c77cc826def6dfc36edad1c9c98d45d9a

为什么看它:它是 Retrofit 请求进入签名前的 Map 归并点。这里能看到 client_salt 被从多个 Map 里移除、aVar.c() 产出 sigaVar.f(sig, salt) 产出 __NStokensig

读完得到的结论sig 不应该从 HAR 复制,而要由排序后的参数重新算;__NStokensigsig + client_salt 的派生;最后还会调用 aVar.e() 给 profile 一次追加签名的机会。

package com.yxcorp.retrofit.utils;

import android.text.TextUtils;
import android.util.Pair;
import java.util.Map;
import nt.RetrofitConfig;
import okhttp3.Request;

/* compiled from: ParamsUtils.java */
/* renamed from: com.yxcorp.retrofit.utils.b, reason: use source file name */
/* loaded from: classes.dex */
public class ParamsUtils {
    private static void a(Map<String, String> map, Map<String, String> map2) {
        for (String str : map.keySet()) {
            if (map.get(str) == null) {
                map.put(str, "");
            }
        }
        if (map2 != null) {
            for (String str2 : map2.keySet()) {
                if (map2.get(str2) == null) {
                    map2.put(str2, "");
                }
            }
        }
    }

    public static Pair<Map<String, String>, Map<String, String>> b(Request request, RetrofitConfig.a aVar, Map<String, String> map, Map<String, String> map2, boolean z2) {
        Map<String, String> b = aVar.b();
        b.putAll(map2);
        Map<String, String> d = aVar.d();
        String remove = map.remove("client_salt");
        if (remove == null) {
            remove = b.remove("client_salt");
        }
        String remove2 = remove == null ? d.remove("client_salt") : remove;
        if (z2) {
            b.putAll(map);
        } else {
            d.putAll(map);
        }
        a(b, d);
        String c = aVar.c(b, d);
        d.put("sig", c);
        if (!TextUtils.isEmpty(remove2)) {
            d.put("__NStokensig", aVar.f(c, remove2));
        }
        aVar.e(request, b, d, remove2, "");
        if (z2) {
            b.putAll(d);
            d.clear();
        }
        return new Pair<>(b, d);
    }
}

对照 Python:把 Java 的 Map 合并翻成可测试算法

Java 里 aVar.c(b, d) 的具体实现后面在 KwaiParams 里看。先把最基础的排序拼接和 token sig 固化在 algorithms.py

def kwai_canonical_params(*maps: Mapping[str, object | None]) -> bytes:
    parts: list[str] = []
    for params in maps:
        for key, value in params.items():
            parts.append(f"{key}={java_value(value)}")
    return "".join(sorted(parts)).encode("utf-8")

def kwai_sig(*maps: Mapping[str, object | None]) -> str:
    return get_clock(kwai_canonical_params(*maps))

def kwai_token_sig(sig: str, client_salt: str) -> str:
    return hashlib.sha256((sig + client_salt).encode("utf-8")).hexdigest()

这里的实现理由很明确:ParamsUtils 不对 key 做业务过滤,而是把进入签名 Map 的内容交给 profile;所以 Python 也不硬编码某个接口字段,而是按请求构造时的 Map 输入统一算。


0x02 跟进 KwaiParams:真正决定 __NS_sig3 输入的是 path + sig

只恢复 sig 还不够。快手主 App 请求里更关键的是 __NS_sig3。最容易踩坑的地方是:它不是对参数串签名,也不是对完整 URL 签名,而是 request.url().h() + sig。这个结论来自 KwaiParams.h(),不能靠猜。

主 App 参数 profile:KwaiParams.java

  • 文件:sources/lo/KwaiParams.java
  • 行数:245
  • 字节:9246
  • SHA256:2b7f3386dc6c715983db189400b78a00093b1efcf85d5d4e23f672676d3091ab

为什么看它:它实现 RetrofitConfig.a,是主 App 公共参数、auth 参数、binary bodyMd5、sig2__NStokensig__NS_sig3 的集中证据。

读完得到的结论:第一个关键点是 WebUtils.a(request.url().h() + sig);第二个关键点是登录态里 tokenkuaishou.api_stclient_salt 的来源;第三个关键点是二进制 body 会额外加 bodyMd5

package lo;

import android.os.Build;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import cj.AppEnv;
import com.antman.utility.g0;
import com.antman.utility.k;
import com.antman.utility.v;
import com.kuaishou.weapon.ks.x;
import com.yxcorp.buildconfig.BuildConfig;
import com.yxcorp.gifshow.log.antman.utils.AbiUtil;
import com.yxcorp.gifshow.log.series.LogSeriesDeviceSampleRatio;
import com.yxcorp.gifshow.util.CPU;
import com.yxcorp.gifshow.util.JsonStringBuilder;
import com.yxcorp.gifshow.util.WebUtils;
import com.yxcorp.init.ChannelInitUtil;
import com.yxcorp.init.LogManagerInitUtil;
import com.yxcorp.utils.antman.AntmanShellSPHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import nt.RetrofitConfig;
import okhttp3.Request;

/* compiled from: KwaiParams.java */
/* renamed from: lo.a, reason: use source file name */
/* loaded from: classes.dex */
public class KwaiParams implements RetrofitConfig.a {
    private static String a;

    private void h(@NonNull Request request, Map<String, String> map) {
        String str = map.get("sig");
        if (TextUtils.isEmpty(str)) {
            return;
        }
        String a2 = WebUtils.a(request.url().h() + str);
        if (str.length() != 32) {
            LogManagerInitUtil.G().a("sig3_fail", String.format("sig3 sig length <> 32 sig[%s]sig3[%s]", str, a2));
        }
        if (TextUtils.isEmpty(a2)) {
            return;
        }
        map.put("__NS_sig3", a2);
    }

    private static String i() {
        if (a == null) {
            try {
                String str = AppEnv.f;
                a = AppEnv.f.substring(0, str.indexOf(".", str.indexOf(".") + 1));
            } catch (Exception unused) {
                a = AppEnv.f;
            }
        }
        return a;
    }

    @Override // nt.RetrofitConfig.a
    @NonNull
    public Map<String, String> a() {
        HashMap hashMap = new HashMap();
        hashMap.put("User-Agent", "kwai-android");
        hashMap.put("Accept-Language", g0.e());
        hashMap.put("X-REQUESTID", String.valueOf(SystemClock.elapsedRealtime()));
        hashMap.put("Connection", "keep-alive");
        AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
        String y2 = antmanShellSPHelper.y();
        if (!TextUtils.isEmpty(y2)) {
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("token=");
            stringBuffer.append(y2);
            hashMap.put("Cookie", stringBuffer.toString());
        }
        if (ChannelInitUtil.c(AppEnv.b)) {
            String v2 = antmanShellSPHelper.v();
            if (!TextUtils.isEmpty(v2)) {
                JsonStringBuilder e3 = JsonStringBuilder.e();
                e3.b("laneId", v2);
                hashMap.put("trace-context", e3.d());
            }
        }
        return hashMap;
    }

    @Override // nt.RetrofitConfig.a
    @NonNull
    public Map<String, String> b() {
        HashMap hashMap = new HashMap();
        String str = LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE;
        hashMap.put("lat", LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE);
        hashMap.put("lon", LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE);
        AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
        if (antmanShellSPHelper.l()) {
            hashMap.put("cl", "1");
        }
        hashMap.put("ver", i());
        hashMap.put("ud", antmanShellSPHelper.x());
        hashMap.put("sys", AppEnv.f1019k);
        hashMap.put(x.f2587p, AppEnv.b);
        hashMap.put("oc", antmanShellSPHelper.p());
        hashMap.put("newOc", AppEnv.c);
        hashMap.put("net", v.d(AppEnv.a));
        hashMap.put("did", antmanShellSPHelper.e());
        hashMap.put("did_tag", String.valueOf(1));
        hashMap.put("cdid_tag", antmanShellSPHelper.c());
        hashMap.put("rdid", antmanShellSPHelper.s());
        if (antmanShellSPHelper.i() != 0) {
            hashMap.put("did_gt", String.valueOf(antmanShellSPHelper.i()));
        }
        hashMap.put("egid", antmanShellSPHelper.h());
        hashMap.put("mod", AppEnv.d);
        if (AppEnv.b.equalsIgnoreCase("GOOGLE_PLAY")) {
            str = "1";
        }
        hashMap.put("app", str);
        hashMap.put("language", g0.e());
        hashMap.put("country_code", antmanShellSPHelper.d());
        hashMap.put("appver", AppEnv.f);
        hashMap.put("ftt", "");
        hashMap.put("iuid", AppEnv.f1018j);
        String r = antmanShellSPHelper.r();
        if (!TextUtils.isEmpty(r)) {
            hashMap.put("pm_tag", r);
        }
        hashMap.put("max_memory", AppEnv.b());
        hashMap.put("kpn", BuildConfig.KPN);
        hashMap.put("apptype", BuildConfig.APP_TYPE);
        hashMap.put("kpf", "ANDROID_PHONE");
        hashMap.put("browseType", "1");
        hashMap.put("isAntman", "true");
        if (AbiUtil.b()) {
            hashMap.put("abi", "arm64");
        } else {
            hashMap.put("abi", "arm32");
        }
        return hashMap;
    }

    @Override // nt.RetrofitConfig.a
    public String c(Map<String, String> map, Map<String, String> map2) {
        ArrayList arrayList = new ArrayList();
        Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
        while (true) {
            String str = "";
            if (!it.hasNext()) {
                break;
            }
            Map.Entry<String, String> next = it.next();
            StringBuilder sb = new StringBuilder();
            sb.append(next.getKey());
            sb.append("=");
            if (next.getValue() != null) {
                str = next.getValue();
            }
            sb.append(str);
            arrayList.add(sb.toString());
        }
        for (Map.Entry<String, String> entry : map2.entrySet()) {
            StringBuilder sb2 = new StringBuilder();
            sb2.append(entry.getKey());
            sb2.append("=");
            sb2.append(entry.getValue() == null ? "" : entry.getValue());
            arrayList.add(sb2.toString());
        }
        try {
            Collections.sort(arrayList);
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        return CPU.a(AppEnv.a, TextUtils.join("", arrayList).getBytes(hw.a.a), Build.VERSION.SDK_INT);
    }

    @Override // nt.RetrofitConfig.a
    @NonNull
    public Map<String, String> d() {
        HashMap hashMap = new HashMap();
        hashMap.put("os", "android");
        hashMap.put("client_key", BuildConfig.CLIENT_KEY);
        AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
        if (antmanShellSPHelper.B()) {
            String y2 = antmanShellSPHelper.y();
            String u2 = antmanShellSPHelper.u();
            String a2 = antmanShellSPHelper.a();
            if (!TextUtils.isEmpty(y2)) {
                hashMap.put("token", y2);
            }
            if (!TextUtils.isEmpty(a2)) {
                hashMap.put("kuaishou.api_st", a2);
            }
            hashMap.put("client_salt", u2);
        }
        return hashMap;
    }

    @Override // nt.RetrofitConfig.a
    public void e(Request request, Map<String, String> map, Map<String, String> map2, String str, String str2) {
        h(request, map2);
    }

    @Override // nt.RetrofitConfig.a
    public String f(String str, String str2) {
        return gw.a.m(str + str2);
    }

    @Override // nt.RetrofitConfig.a
    public Map<String, String> g(Map<String, String> map, byte[] bArr) {
        Map<String, String> b = b();
        HashMap hashMap = new HashMap();
        HashMap hashMap2 = (HashMap) b;
        hashMap2.put("os", "android");
        hashMap2.put("client_key", BuildConfig.CLIENT_KEY);
        for (Map.Entry<String, String> entry : map.entrySet()) {
            hashMap2.put(entry.getKey(), entry.getValue());
        }
        hashMap2.put("bodyMd5", k.d(bArr));
        AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
        if (antmanShellSPHelper.B()) {
            hashMap.put("token", antmanShellSPHelper.y());
            String y2 = antmanShellSPHelper.y();
            String a2 = antmanShellSPHelper.a();
            if (!TextUtils.isEmpty(y2)) {
                hashMap.put("token", y2);
            }
            if (!TextUtils.isEmpty(a2)) {
                hashMap.put("kuaishou.api_st", a2);
            }
        }
        for (Map.Entry entry2 : hashMap2.entrySet()) {
            hashMap.put((String) entry2.getKey(), (String) entry2.getValue());
        }
        String c = c(hashMap, new HashMap<>());
        hashMap2.put("sig2", c);
        AntmanShellSPHelper antmanShellSPHelper2 = AntmanShellSPHelper.a;
        String u2 = antmanShellSPHelper2.u();
        if (antmanShellSPHelper2.B() && !TextUtils.isEmpty(u2)) {
            hashMap2.put("__NStokensig", f(c, u2));
        }
        hashMap2.remove("bodyMd5");
        return b;
    }
}

这里为什么能定死 sig3_input = path + sig

关键 Java 逻辑是:

String str = map.get("sig");
String a2 = WebUtils.a(request.url().h() + str);
map.put("__NS_sig3", a2);

这三行对应 Python 的请求签名过程:先算 sig,再拼 path + sig,最后调用 make_sig3()。这也是之前跑 path matrix / sig3 input matrix 时最关键的判断依据。


0x03 Java 层只是一层壳:WebUtilsKSecurity

KwaiParams 为止,我们知道了送进 sig3 的字符串是什么,但还不知道算法。继续跟 WebUtils.a()。这一步很像 2zxz 文里的写法:搜到一个可疑函数,不急着下结论,先看它到底有没有算法。结果很明显,Java 层没有算法,只是调用 KSecurity。

WebUtils.a():把字符串送进 KSecurity

  • 文件:sources/com/yxcorp/gifshow/util/WebUtils.java
  • 行数:40
  • 字节:1490
  • SHA256:1dd741f4382fd5afeee8b179a90003f431ded4fd34d03a0ad771478ecb24ad24

为什么看它:它是 KwaiParams 和安全 SDK 的连接点。

读完得到的结论WebUtils.a() 只做空值、耗时、异常日志,然后调用 KSecurity.atlasSign(str)。算法不在这里。

package com.yxcorp.gifshow.util;

import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.yxcorp.init.LogManagerInitUtil;
import kotlin.jvm.JvmStatic;
import kotlin.jvm.internal.h;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/* compiled from: WebUtils.kt */
/* renamed from: com.yxcorp.gifshow.util.c, reason: use source file name */
/* loaded from: classes.dex */
public final class WebUtils {
    @JvmStatic
    @NotNull
    public static final String a(@Nullable String str) {
        if (TextUtils.isEmpty(str)) {
            return "";
        }
        try {
            long elapsedRealtime = SystemClock.elapsedRealtime();
            h.c(str);
            String atlasSign = KSecurity.atlasSign(str);
            h.e(atlasSign, "atlasSign(sig!!)");
            LogManagerInitUtil.G().a("sig3_cost", "" + (SystemClock.elapsedRealtime() - elapsedRealtime));
            return atlasSign;
        } catch (Exception e3) {
            if (!(e3 instanceof KSException)) {
                LogManagerInitUtil.G().a("sig3_error", Log.getStackTraceString(e3));
                return "";
            }
            LogManagerInitUtil.G().a("sig3_fail", ((KSException) e3).getErrorCode() + '_' + e3.getMessage());
            return "";
        }
    }
}

KSecurity 公开 API:KSecurity.java

  • 文件:sources/com/kuaishou/android/security/KSecurity.java
  • 行数:242
  • 字节:8499
  • SHA256:b2ae31588f548644b7e8733a37d8e549cff4773b4869813f65b064432c38727e

为什么看它:它暴露 atlasSignatlasSignPlusatlasSignPlusInerInitializegetSecurityValue 等 API,是 Java 进入 bridge/native 前最后一层清晰入口。

读完得到的结论atlasSign(str) 实际调用 b.i().d(str, false, "")atlasSignPlus 和 inner 分支用于别的安全场景,DFP/MXSec 会用到类似能力。

package com.kuaishou.android.security;

import android.content.Context;
import androidx.annotation.NonNull;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.KSecuritySdkILog;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.base.util.n;
import com.kuaishou.android.security.bridge.main.b;
import com.kuaishou.android.security.features.drm.DrmContext;
import com.kuaishou.android.security.internal.common.KSecurityContext;
import com.kuaishou.android.security.internal.common.h;
import com.kuaishou.android.security.internal.dispatch.JNICLibrary;
import com.kuaishou.android.security.internal.init.SecDidProxy;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONObject;

/* loaded from: classes.dex */
public class KSecurity {
    private static long a;
    private static long b;
    private static AtomicBoolean c = new AtomicBoolean(true);
    private static KSecuritySdkILog d = null;

    public enum ENV {
        ROOT(0),
        MALWARE(1),
        HOOK(2),
        EMULATOR(3),
        ANTIDEBUG(4),
        REPACK(5);

        private final int value;

        ENV(int i2) {
            this.value = i2;
        }

        public int getIntValue() {
            return this.value;
        }
    }

    public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull KSecuritySdkILog kSecuritySdkILog) throws KSException {
        int a2;
        synchronized (KSecurity.class) {
            a = System.currentTimeMillis();
            b.i().h().setProductName("");
            b.i().h().setWithFeature(KSecurityContext.Feature.GUARD);
            b.i().h().setDid("");
            a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(KSecurityContext.Mode.ASYNC).a(KSecurityTrack.getDelegateCb()));
        }
        return a2;
    }

    public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull KSecuritySdkILog kSecuritySdkILog) throws KSException {
        int Initialize;
        synchronized (KSecurity.class) {
            Initialize = Initialize(context, str, str2, str3, str4, "", "", kSecuritySdkILog, KSecurityContext.Mode.ASYNC);
        }
        return Initialize;
    }

    public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
        int Initialize;
        synchronized (KSecurity.class) {
            a = System.currentTimeMillis();
            Initialize = Initialize(context, str, str2, str3, str4, "", "", kSecuritySdkILog, mode);
        }
        return Initialize;
    }

    public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull String str5, @NonNull String str6, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
        int a2;
        synchronized (KSecurity.class) {
            a = System.currentTimeMillis();
            b.i().h().setProductName(str3);
            b.i().h().setWithFeature(KSecurityContext.Feature.ALL);
            b.i().h().setDid(str4);
            b.i().h().setRdid(str5);
            b.i().h().setRdidtag(str6);
            a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(mode).a(KSecurityTrack.getDelegateCb()));
        }
        return a2;
    }

    public static synchronized int InitializeKuaiShou(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, SecDidProxy secDidProxy, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
        int a2;
        synchronized (KSecurity.class) {
            a = System.currentTimeMillis();
            a = System.currentTimeMillis();
            b.i().h().setProductName(str3);
            b.i().h().setWithFeature(KSecurityContext.Feature.ALL);
            b.i().h().setSecDidProxy(secDidProxy);
            a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(mode).a(KSecurityTrack.getDelegateCb()));
        }
        return a2;
    }

    @NonNull
    @Deprecated
    public static byte[] asymmetricDecrypt(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar) throws KSException {
        return b.i().a(bArr, bVar, true, false, "");
    }

    @NonNull
    @Deprecated
    public static byte[] asymmetricEncrypt(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar) throws KSException {
        return b.i().b(bArr, bVar, true, false, "");
    }

    @NonNull
    public static byte[] atlasDecrypt(@NonNull byte[] bArr) throws KSException {
        return b.i().b(bArr, false, "");
    }

    @NonNull
    public static byte[] atlasEncrypt(@NonNull byte[] bArr) throws KSException {
        return b.i().a(bArr, false, "");
    }

    @NonNull
    public static String atlasSign(@NonNull String str) throws KSException {
        String d3 = b.i().d(str, false, "");
        c.getAndSet(false);
        return d3;
    }

    @NonNull
    public static String atlasSignPlus(@NonNull String str) throws KSException {
        return b.i().a(str, false, "");
    }

    @NonNull
    public static String atlasSignPlusIner(@NonNull String str, @NonNull String str2) throws KSException {
        return b.i().a(str, true, str2);
    }

    @NonNull
    public static String challenge(@NonNull String str) throws KSException {
        return b.i().c(str, false, "");
    }

    @NonNull
    @Deprecated
    public static String checkEnv(@NonNull String str) throws KSException {
        return b.i().b(str, false, "");
    }

    @NonNull
    public static boolean detectEnvironment(@NonNull ENV env) throws KSException {
        return b.i().a(env, false, "");
    }

    public static void doSentiveWork(boolean z2) throws KSException {
    }

    public static long getAppStartTime() {
        return b;
    }

    public static long getAppTime() {
        return b;
    }

    public static long getDRMBridgeFuncAddr() {
        if (isInitialize()) {
            return JNICLibrary.gDBF();
        }
        return 0L;
    }

    public static long getInitTime() {
        return a;
    }

    public static long getKSBridgeFuncAddr() {
        if (isInitialize()) {
            return JNICLibrary.gKSF();
        }
        return 0L;
    }

    @NonNull
    public static String getSecurityValue(@NonNull int i2) throws KSException {
        return b.i().a(i2, false, "");
    }

    public static KSecurityContext getkSecurityParameterContext() {
        return b.i().h();
    }

    public static boolean isInitialize() {
        return b.i().g();
    }

    @NonNull
    public static String localChallenge(String str) throws KSException {
        return b.i().a(false, "", str);
    }

    public static void sendReportLog(Context context, String str, String str2, JSONObject jSONObject) {
        n.a(str2, jSONObject.toString());
    }

    public static void setAppStartTime(long j3) {
        b = j3;
    }

    public static void setAppTime(long j3) {
        b = j3;
    }

    public static void setDRMUserinfo(@NonNull String str, @NonNull String str2, @NonNull String str3) {
        DrmContext.setDid(str3);
        DrmContext.setUid(str2);
        DrmContext.setToken(str);
    }

    public static void setDrmDebugHost(boolean z2) {
        DrmContext.setIsDebugModel(z2);
    }

    public static void setInitTime(long j3) {
        a = j3;
    }

    public static void setKCObject(Map<String, Boolean> map) {
        b.i().h().setKeyconfigMap(map);
    }

    @NonNull
    public static byte[] uDecrypt(@NonNull byte[] bArr) throws KSException {
        return b.i().a(bArr, false, false, "");
    }

    @NonNull
    public static byte[] uEncrypt(@NonNull byte[] bArr) throws KSException {
        return b.i().b(bArr, false, false, "");
    }
}

security bridge 主类:bridge/main/a.java

  • 文件:sources/com/kuaishou/android/security/bridge/main/a.java
  • 行数:375
  • 字节:17889
  • SHA256:3cd86d671937eeb14ac7d92f6574e8f74aed469c62d202e49f0991c0f7594bb7

为什么看它:这里能看到未初始化、参数非法、环境异常时的错误处理和安全日志;它解释为什么实机 hook/未初始化会出现空串或异常。

读完得到的结论:Java bridge 不是算法主体,但它决定是否能成功进入 native,以及失败时返回什么错误。

package com.kuaishou.android.security.bridge.main;

import androidx.annotation.NonNull;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.perf.a;
import com.kuaishou.android.security.base.perf.d;
import com.kuaishou.android.security.base.perf.j;
import com.kuaishou.android.security.base.perf.k;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.internal.common.c;
import com.kuaishou.android.security.internal.common.g;
import com.kuaishou.livestream.message.nano.LivePetMessages;

/* loaded from: classes.dex */
public abstract class a extends com.kuaishou.android.security.internal.dispatch.b implements g {

    /* renamed from: com.kuaishou.android.security.bridge.main.a$a, reason: collision with other inner class name */
    public static /* synthetic */ class C0048a {
        public static final /* synthetic */ int[] a;

        static {
            int[] iArr = new int[KSecurity.ENV.values().length];
            a = iArr;
            try {
                iArr[KSecurity.ENV.ROOT.ordinal()] = 1;
            } catch (NoSuchFieldError unused) {
            }
            try {
                a[KSecurity.ENV.MALWARE.ordinal()] = 2;
            } catch (NoSuchFieldError unused2) {
            }
            try {
                a[KSecurity.ENV.HOOK.ordinal()] = 3;
            } catch (NoSuchFieldError unused3) {
            }
            try {
                a[KSecurity.ENV.EMULATOR.ordinal()] = 4;
            } catch (NoSuchFieldError unused4) {
            }
            try {
                a[KSecurity.ENV.ANTIDEBUG.ordinal()] = 5;
            } catch (NoSuchFieldError unused5) {
            }
            try {
                a[KSecurity.ENV.REPACK.ordinal()] = 6;
            } catch (NoSuchFieldError unused6) {
            }
        }
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String a(@NonNull int i2, boolean z2, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1739k) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1739k).booleanValue()) {
                    return "";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] getSecurityValue dont Init [%s]", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        String a2 = e().a(i2, z2, str);
        k.a(a.EnumC0044a.GETSVALUE, a);
        return a2;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String a(@NonNull String str, boolean z2, String str2) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1741m) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1741m).booleanValue()) {
                    return "";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        long currentTimeMillis = System.currentTimeMillis();
        if (g()) {
            if (str.length() == 0) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasSignPlus invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), 11106));
                return "";
            }
            try {
                return d().c(str, z2, str2);
            } catch (Throwable th) {
                th.printStackTrace();
                d.a(d.b.ALL, th.getLocalizedMessage(), 11100);
                return d().b(str, z2, str2);
            }
        }
        KSecurityTrack.sLog(99, currentTimeMillis);
        KSecurityTrack.sLog(-99);
        String format = String.format("[KGE]KWSecurity[%b] atlassignPlus dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
        KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
        if (b.i().j() != null) {
            b.i().j().e().onSeucrityError(kSException);
        }
        d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
        throw kSException;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String a(boolean z2, String str, String str2) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1742n) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1742n).booleanValue()) {
                    return "";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        long currentTimeMillis = System.currentTimeMillis();
        if (g()) {
            return d().a(z2, str, str2);
        }
        KSecurityTrack.sLog(99, currentTimeMillis);
        KSecurityTrack.sLog(-99);
        String format = String.format("[KGE]KWSecurity[%b] local challenge dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
        KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
        if (b.i().j() != null) {
            b.i().j().e().onSeucrityError(kSException);
        }
        d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
        throw kSException;
    }

    /* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
    /* JADX WARN: Removed duplicated region for block: B:64:0x012a A[RETURN] */
    @Override // com.kuaishou.android.security.internal.common.g
    @androidx.annotation.NonNull
    /*
        Code decompiled incorrectly, please refer to instructions dump.
        To view partially-correct code enable 'Show inconsistent code' option in preferences
    */
    public boolean a(com.kuaishou.android.security.KSecurity.ENV r8, boolean r9, java.lang.String r10) throws com.kuaishou.android.security.base.exception.KSException {
        /*
            Method dump skipped, instructions count: 316
            To view this dump change 'Code comments level' option to 'DEBUG'
        */
        throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.bridge.main.a.a(com.kuaishou.android.security.KSecurity$ENV, boolean, java.lang.String):boolean");
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] a(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar, boolean z2, boolean z3, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.r) != null && b.i().h().getKeyconfigMap().get(c.r).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] asymmetricDecrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] a2 = a().a(bArr, bVar, z2, z3);
        k.a(a.EnumC0044a.ASMDEC, a);
        return a2;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] a(@NonNull byte[] bArr, boolean z2, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f) != null && b.i().h().getKeyconfigMap().get(c.f).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasEncrypt dont Init [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] b = b().b(bArr, true, z2, str);
        k.a(a.EnumC0044a.AENC, a);
        return b;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] a(@NonNull byte[] bArr, boolean z2, boolean z3, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1737i) != null && b.i().h().getKeyconfigMap().get(c.f1737i).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] uDecrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] c = b().c(bArr, z2, z3, str);
        k.a(a.EnumC0044a.UDEC, a);
        return c;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String b(@NonNull String str, boolean z2, String str2) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1744p) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1744p).booleanValue()) {
                    return "0:0:0:0:0:0";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!KSecurity.isInitialize() && b.i().j() != null) {
            b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] CheckEnv dont Init [%s]", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
        }
        j a = k.a();
        String a2 = c().a(str);
        k.a(a.EnumC0044a.ENV, a);
        return a2;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] b(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar, boolean z2, boolean z3, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.q) != null && b.i().h().getKeyconfigMap().get(c.q).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] asymmetricEncrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] b = a().b(bArr, bVar, z2, z3);
        k.a(a.EnumC0044a.ASMENC, a);
        return b;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] b(@NonNull byte[] bArr, boolean z2, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1735g) != null && b.i().h().getKeyconfigMap().get(c.f1735g).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g() && b.i().j() != null) {
            b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] atlasDecrypt not Init [%s] ", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] a2 = b().a(bArr, false, z2, str);
        k.a(a.EnumC0044a.ADEC, a);
        return a2;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public byte[] b(@NonNull byte[] bArr, boolean z2, boolean z3, String str) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1736h) != null && b.i().h().getKeyconfigMap().get(c.f1736h).booleanValue()) {
                return new byte[0];
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        if (!g()) {
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] uEncrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
            }
            throw new KSException(LivePetMessages.LivePetActionType.THINK);
        }
        j a = k.a();
        byte[] d = b().d(bArr, z2, z3, str);
        k.a(a.EnumC0044a.UENC, a);
        return d;
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String c(@NonNull String str, boolean z2, String str2) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1745s) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1745s).booleanValue()) {
                    return "";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        long currentTimeMillis = System.currentTimeMillis();
        if (!g()) {
            KSecurityTrack.sLog(99, currentTimeMillis);
            KSecurityTrack.sLog(-99);
            String format = String.format("[KGE]KWSecurity[%b] challenge dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
            KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
            if (b.i().j() != null) {
                b.i().j().e().onSeucrityError(kSException);
            }
            d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
            throw kSException;
        }
        if (str.length() == 0) {
            b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity challenge invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.SAD));
            return "";
        }
        String a = d().a(str, z2, str2);
        if (a != null && !a.isEmpty()) {
            return a;
        }
        d.a(d.b.ALL, "challenge ret empty or null", LivePetMessages.LivePetActionType.THINK);
        return "";
    }

    @Override // com.kuaishou.android.security.internal.common.g
    @NonNull
    public String d(@NonNull String str, boolean z2, String str2) throws KSException {
        try {
            if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1740l) != null) {
                if (b.i().h().getKeyconfigMap().get(c.f1740l).booleanValue()) {
                    return "";
                }
            }
        } catch (Exception e3) {
            e3.printStackTrace();
        }
        long currentTimeMillis = System.currentTimeMillis();
        if (g()) {
            if (str.length() != 0) {
                return new com.kuaishou.android.security.base.util.c(b.i().j().c()).m() ? d().b(str, z2, str2) : d().c(str, z2, str2);
            }
            b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasSign invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.SAD));
            return "";
        }
        KSecurityTrack.sLog(99, currentTimeMillis);
        KSecurityTrack.sLog(-99);
        String format = String.format("[KGE]KWSecurity[%b] atlassign dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
        KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
        if (b.i().j() != null) {
            b.i().j().e().onSeucrityError(kSException);
        }
        d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
        throw kSException;
    }

    public abstract boolean g();
}

middleware bridge:bridge/middleware/a.java

  • 文件:sources/com/kuaishou/android/security/bridge/middleware/a.java
  • 行数:186
  • 字节:6458
  • SHA256:8085c72053ed1f9e8ee9f10f1b99fc87eee77e706c3b3d2c361ebc77e0ae35be

为什么看它:它展示 IKSecurityBase 包装的 atlasSignatlasEncryptdfpCalldetectEnvironment

读完得到的结论:同一个安全 SDK 能力不只服务 REST sig3,也服务 DFP/MXSec/环境检测。

package com.kuaishou.android.security.bridge.middleware;

import android.content.Context;
import androidx.annotation.NonNull;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.d;
import com.kuaishou.android.security.base.util.n;
import com.kuaishou.android.security.bridge.main.b;
import com.middleware.security.configs.EnvironmentType;
import com.middleware.security.wrapper.SDKType;
import org.json.JSONObject;
import ph.IDfpSupplier;
import ph.IKSecurityBase;
import ph.IPolicyListener;

/* loaded from: classes.dex */
public class a extends com.kuaishou.android.security.bridge.main.a implements IKSecurityBase {
    @Override // ph.IKSecurityBase
    public byte[] atlasDecrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
        try {
            return b(bArr, true, str2);
        } catch (KSException unused) {
            return new byte[0];
        }
    }

    @Override // ph.IKSecurityBase
    public byte[] atlasEncrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
        try {
            return a(bArr, true, str2);
        } catch (KSException unused) {
            return new byte[0];
        }
    }

    @Override // ph.IKSecurityBase
    public String atlasSign(String str, String str2, @SDKType int i2, String str3) {
        try {
            return d(str3, true, str2);
        } catch (KSException unused) {
            return "";
        }
    }

    public String atlasSignLite(String str, String str2, @SDKType int i2, String str3) {
        try {
            return a(str3, true, str2);
        } catch (KSException unused) {
            return "";
        }
    }

    @Override // ph.IKSecurityBase
    public String challenge(String str, String str2, @SDKType int i2, String str3) {
        try {
            return c(str3, true, str2);
        } catch (KSException unused) {
            return "";
        }
    }

    public boolean detectEnvironment(String str, String str2, @SDKType int i2, @EnvironmentType int i3) {
        KSecurity.ENV env;
        switch (i3) {
            case 0:
                env = KSecurity.ENV.ROOT;
                break;
            case 1:
                env = KSecurity.ENV.MALWARE;
                break;
            case 2:
                env = KSecurity.ENV.HOOK;
                break;
            case 3:
                env = KSecurity.ENV.EMULATOR;
                break;
            case 4:
                env = KSecurity.ENV.ANTIDEBUG;
                break;
            case 5:
                env = KSecurity.ENV.REPACK;
                break;
            case 6:
                return KSecurity.isInitialize();
            default:
                return false;
        }
        return a(env, true, str2);
    }

    /* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
    @Override // ph.IKSecurityBase
    public Object dfpCall(int i2, Object... objArr) {
        try {
            if (i2 == 1114118) {
                com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).c();
            } else if (i2 == 1114119) {
                com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).b();
            } else {
                if (i2 == 1114127) {
                    return com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).a();
                }
                switch (i2) {
                    case 1114113:
                        if (objArr != null) {
                            Context c = b.i().j().c();
                            com.kuaishou.android.security.base.cloudconfig.b.a(c).a((IPolicyListener) objArr[0]);
                            com.kuaishou.android.security.base.cloudconfig.b.a(c).a(true);
                            break;
                        }
                        break;
                    case 1114114:
                        if (objArr != null) {
                            com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).a((IDfpSupplier) objArr[0]);
                            break;
                        }
                        break;
                    case 1114115:
                        if (objArr != null) {
                            n.a((String) objArr[0], (String) objArr[1]);
                            break;
                        }
                        break;
                    case 1114116:
                        if (objArr != null) {
                            n.a((String) objArr[0], (String) objArr[1], ((Boolean) objArr[2]).booleanValue(), ((Boolean) objArr[3]).booleanValue());
                            break;
                        }
                        break;
                }
            }
        } catch (Throwable th) {
            d.a(th);
        }
        return null;
    }

    @Override // com.kuaishou.android.security.bridge.main.a
    public boolean g() {
        if (b.i().g()) {
            return true;
        }
        return f().a();
    }

    @Override // ph.IKSecurityBase
    public String getSecurityValue(String str, String str2, @SDKType int i2, int i3) {
        try {
            return a(i3, true, str2);
        } catch (KSException unused) {
            return "";
        }
    }

    @Override // ph.IKSecurityBase
    public String localChallenge(String str, String str2, @SDKType int i2, String str3) {
        try {
            return a(true, str2, str3);
        } catch (KSException unused) {
            return "";
        }
    }

    public void parseKConfPolicy(@NonNull JSONObject jSONObject) {
        com.kuaishou.android.security.base.cloudconfig.a.a(b.i().j().c()).f(jSONObject);
    }

    @Override // ph.IKSecurityBase
    public byte[] uDecrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
        try {
            return a(bArr, false, true, str2);
        } catch (KSException unused) {
            return new byte[0];
        }
    }

    @Override // ph.IKSecurityBase
    public byte[] uEncrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
        try {
            return b(bArr, false, true, str2);
        } catch (KSException unused) {
            return new byte[0];
        }
    }
}

native loader / dispatch 边界:internal/dispatch/c.java

  • 文件:sources/com/kuaishou/android/security/internal/dispatch/c.java
  • 行数:422
  • 字节:18247
  • SHA256:499e64305c59ef0ebbb758ee18161d6e386a23a3d7037284d306aac01218b3e0

为什么看它:它负责 native so 的名字、释放、校验、加载。这里能看到 kwsgmain

读完得到的结论:只看 Java 一定不够;签名主体在 libkwsgmain 这类 native 库里。

package com.kuaishou.android.security.internal.dispatch;

import android.content.Context;
import android.util.Base64;
import androidx.annotation.RequiresApi;
import com.kuaishou.android.security.base.perf.d;
import com.kuaishou.android.security.base.perf.i;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.base.util.j;
import com.kwai.plugin.dva.loader.SoLoader;
import com.kwai.plugin.dva.split.LibraryInstaller;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/* loaded from: classes.dex */
public class c {
    private static boolean a = false;
    private static String b = "kwsgmain";

    private static InputStream a(ZipFile zipFile, String str) {
        InputStream b2 = b(zipFile, str);
        return (b2 == null && str.equals("armeabi")) ? b(zipFile, LibraryInstaller.ARM32) : b2;
    }

    public static String a() {
        return b;
    }

    public static String a(File file) throws FileNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(file);
        String str = null;
        try {
            try {
                MappedByteBuffer map = fileInputStream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0L, file.length());
                MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                messageDigest.update(map);
                str = new BigInteger(1, messageDigest.digest()).toString(16);
            } catch (Exception e3) {
                e3.printStackTrace();
            }
            return str;
        } finally {
            try {
                fileInputStream.close();
            } catch (IOException e5) {
                e5.printStackTrace();
            }
        }
    }

    public static String a(byte[] bArr) throws FileNotFoundException {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(bArr);
            return new BigInteger(1, messageDigest.digest()).toString(16);
        } catch (Exception e3) {
            e3.printStackTrace();
            return null;
        }
    }

    /* JADX WARN: Code restructure failed: missing block: B:51:0x0167, code lost:
    
        if (r3 == null) goto L40;
     */
    /*
        Code decompiled incorrectly, please refer to instructions dump.
        To view partially-correct code enable 'Show inconsistent code' option in preferences
    */
    private static void a(android.content.Context r19, java.lang.Throwable r20) {
        /*
            Method dump skipped, instructions count: 405
            To view this dump change 'Code comments level' option to 'DEBUG'
        */
        throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.internal.dispatch.c.a(android.content.Context, java.lang.Throwable):void");
    }

    private static void a(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (IOException e3) {
            e3.printStackTrace();
        }
    }

    public static boolean a(Context context) {
        boolean z2 = a;
        if (z2) {
            return z2;
        }
        try {
            KSecurityTrack.sLog(29);
            com.kuaishou.android.security.internal.loader.b.a(b);
            a = true;
        } catch (Throwable th) {
            com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("Loadlibrary exception lib[%s]\r\n throw[%s]\r\n", b, th.getMessage()), com.kuaishou.android.security.base.perf.b.f1568t);
            a(context, th);
            a = false;
        }
        if (a) {
            KSecurityTrack.sLog(31);
        } else {
            if (!b(context)) {
                KSecurityTrack.sLog(32);
                return false;
            }
            KSecurityTrack.sLog(33);
            com.kuaishou.android.security.base.log.d.a("retry Load so ok");
        }
        return a;
    }

    private static boolean a(BufferedInputStream bufferedInputStream, BufferedInputStream bufferedInputStream2) {
        try {
            int available = bufferedInputStream.available();
            int available2 = bufferedInputStream2.available();
            if (available != available2) {
                return false;
            }
            byte[] bArr = new byte[available];
            byte[] bArr2 = new byte[available2];
            bufferedInputStream.read(bArr);
            bufferedInputStream2.read(bArr2);
            for (int i2 = 0; i2 < available; i2++) {
                if (bArr[i2] != bArr2[i2]) {
                    return false;
                }
            }
            return true;
        } catch (FileNotFoundException e3) {
            e3.printStackTrace();
            return false;
        } catch (IOException e5) {
            e5.printStackTrace();
            return false;
        }
    }

    public static boolean a(String str, String str2, File file) throws IOException {
        boolean b2 = b(str, str2, file);
        return (b2 || !str2.equals("armeabi")) ? b2 : b(str, LibraryInstaller.ARM32, file);
    }

    @RequiresApi(api = 19)
    public static boolean a(String str, String str2, String str3) {
        FileInputStream fileInputStream;
        ZipFile zipFile;
        InputStream a2;
        boolean z2 = false;
        try {
            fileInputStream = new FileInputStream(str3);
            try {
                zipFile = new ZipFile(str);
                try {
                    a2 = a(zipFile, str2);
                } finally {
                }
            } finally {
            }
        } catch (IOException unused) {
        }
        if (a2 == null) {
            if (a2 != null) {
                a2.close();
            }
            zipFile.close();
            fileInputStream.close();
            return false;
        }
        try {
            z2 = a(new BufferedInputStream(a2), new BufferedInputStream(fileInputStream));
            a2.close();
            zipFile.close();
            fileInputStream.close();
            return z2;
        } finally {
        }
    }

    @RequiresApi(api = 19)
    private static boolean a(ZipInputStream zipInputStream, File file) {
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file, false);
            try {
                byte[] bArr = new byte[1024];
                while (true) {
                    int read = zipInputStream.read(bArr);
                    if (read <= 0) {
                        fileOutputStream.flush();
                        fileOutputStream.close();
                        fileOutputStream.close();
                        return true;
                    }
                    fileOutputStream.write(bArr, 0, read);
                }
            } finally {
            }
        } catch (IOException unused) {
            return false;
        }
    }

    private static InputStream b(ZipFile zipFile, String str) {
        try {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry nextElement = entries.nextElement();
                String name = nextElement.getName();
                StringBuilder sb = new StringBuilder();
                sb.append(SoLoader.APK_LIB_DIR_PREFIX);
                sb.append(str);
                sb.append("/");
                sb.append(String.format("lib%s.so", a()));
                if (name.equals(sb.toString()) && !str.contains("../")) {
                    return zipFile.getInputStream(nextElement);
                }
            }
        } catch (IOException e3) {
            e3.printStackTrace();
        }
        return null;
    }

    private static boolean b() {
        String str = "";
        try {
            String str2 = new String(Base64.decode("aHR0cDovL3N0YXRpYy55eGltZ3MuY29tL3VkYXRhL3BrZy9rd2FpLWNsaWVudC1pbWFnZS9zYWlvX2d1YXYyX3JhbmRvbXNz", 0));
            String str3 = new String(Base64.decode("bGlia3dzZ21haW4uc28=", 0));
            String str4 = com.kuaishou.android.security.bridge.main.b.i().j().c().getFilesDir().getAbsolutePath() + File.separator + "seaio/" + str3;
            str = String.format("\t\t%s[%s]\r\n", str4, a(j.a(str2, str3)));
            System.load(str4);
            KSecurityTrack.sLog(38);
            com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 3 success" + str, new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
            return true;
        } catch (Throwable th) {
            KSecurityTrack.sLog(38);
            com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 3 other exception [%s][%s]", str, th.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
            return false;
        }
    }

    private static boolean b(Context context) {
        boolean z2;
        String packageResourcePath;
        String a2;
        if (context == null) {
            return false;
        }
        File file = new File(context.getFilesDir().getPath(), "KWSG_LIB");
        File file2 = new File(file.getAbsolutePath(), String.format("lib%.so", a()));
        if (!file.exists()) {
            file.mkdir();
        }
        try {
            packageResourcePath = context.getPackageResourcePath();
            a2 = i.a();
        } catch (Throwable th) {
            KSecurityTrack.sLog(38);
            com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load other exception [%s]", th.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
            z2 = false;
        }
        if (a2 == null) {
            KSecurityTrack.sLog(34);
            return false;
        }
        a = false;
        if (file2.exists()) {
            if (a(packageResourcePath, a2, file2.getAbsolutePath())) {
                try {
                    KSecurityTrack.sLog(35);
                    System.load(file2.getAbsolutePath());
                    a = true;
                    com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 1 success", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
                } catch (Throwable th2) {
                    KSecurityTrack.sLog(36);
                    com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 1 [%s]", th2.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
                    a = false;
                }
            } else {
                file2.delete();
            }
        }
        if (a) {
            KSecurityTrack.sLog(37);
            return a;
        }
        z2 = a(packageResourcePath, a2, file);
        if (z2) {
            try {
                KSecurityTrack.sLog(39);
                System.load(file2.getAbsolutePath());
                a = true;
                com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 2 success", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
            } catch (Throwable th3) {
                KSecurityTrack.sLog(40);
                com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 2 [%s]", th3.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
                a = false;
            }
        } else {
            com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load unzip so failure", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
        }
        return a;
    }

    /* JADX WARN: Removed duplicated region for block: B:19:0x0076 A[Catch: all -> 0x008b, Exception -> 0x008e, TryCatch #6 {Exception -> 0x008e, all -> 0x008b, blocks: (B:6:0x0010, B:8:0x0018, B:10:0x0031, B:12:0x004a, B:14:0x0064, B:19:0x0076, B:22:0x007a, B:26:0x007f), top: B:5:0x0010 }] */
    /*
        Code decompiled incorrectly, please refer to instructions dump.
        To view partially-correct code enable 'Show inconsistent code' option in preferences
    */
    private static boolean b(java.lang.String r9, java.lang.String r10, java.io.File r11) throws java.io.IOException {
        /*
            r0 = 0
            java.util.zip.ZipInputStream r1 = new java.util.zip.ZipInputStream     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            java.io.FileInputStream r2 = new java.io.FileInputStream     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            java.io.File r3 = new java.io.File     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            r3.<init>(r9)     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            r2.<init>(r3)     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            r1.<init>(r2)     // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
            java.util.zip.ZipEntry r9 = r1.getNextEntry()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r0 = 0
            r2 = 0
        L16:
            if (r9 == 0) goto L7f
            java.lang.String r3 = r9.getName()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.StringBuilder r4 = new java.lang.StringBuilder     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r4.<init>()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r5 = "lib/"
            r4.append(r5)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r4.append(r10)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r5 = "/"
            r4.append(r5)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r5 = "lib%s.so"
            r6 = 1
            java.lang.Object[] r7 = new java.lang.Object[r6]     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r8 = a()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r7[r0] = r8     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r5 = java.lang.String.format(r5, r7)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r4.append(r5)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.lang.String r4 = r4.toString()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            boolean r3 = r3.equals(r4)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            if (r3 == 0) goto L7a
            java.lang.String r2 = r9.getName()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r3 = 47
            int r3 = r2.lastIndexOf(r3)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            int r3 = r3 + r6
            java.lang.String r2 = r2.substring(r3)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            java.io.File r3 = new java.io.File     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r3.<init>(r11, r2)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            boolean r2 = r3.exists()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            if (r2 == 0) goto L73
            long r4 = r3.lastModified()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            long r7 = r9.getTime()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            int r9 = (r4 > r7 ? 1 : (r4 == r7 ? 0 : -1))
            if (r9 >= 0) goto L71
            goto L73
        L71:
            r9 = 0
            goto L74
        L73:
            r9 = 1
        L74:
            if (r9 == 0) goto L79
            a(r1, r3)     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
        L79:
            r2 = 1
        L7a:
            java.util.zip.ZipEntry r9 = r1.getNextEntry()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            goto L16
        L7f:
            r1.close()     // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
            r1.close()     // Catch: java.lang.Exception -> L86
            goto L8a
        L86:
            r9 = move-exception
            r9.printStackTrace()
        L8a:
            return r2
        L8b:
            r9 = move-exception
            r0 = r1
            goto L95
        L8e:
            r9 = move-exception
            r0 = r1
            goto L94
        L91:
            r9 = move-exception
            goto L95
        L93:
            r9 = move-exception
        L94:
            throw r9     // Catch: java.lang.Throwable -> L91
        L95:
            if (r0 == 0) goto L9f
            r0.close()     // Catch: java.lang.Exception -> L9b
            goto L9f
        L9b:
            r10 = move-exception
            r10.printStackTrace()
        L9f:
            throw r9
        */
        throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.internal.dispatch.c.b(java.lang.String, java.lang.String, java.io.File):boolean");
    }
}

0x04 进入 IDA:doCommandNative 和三个 sig3 command

Java 已经证明 path + sig 会进入 KSecurity.atlasSign(),但算法细节必须进 IDA。当前工作区已经导出了关键函数的 Hex-Rays 伪代码,所以这里直接内联。

注意:IDA 伪代码里的变量名大多是 v5/v14/v39,还有大量 opaque predicate;分析时不要被变量声明和混淆循环拖走,重点看 command id、case、trampoline、输入是否为空、输出是否写回。

JNI dispatcher:ida_jni_doCommandNative_dispatcher.c

  • 文件:output/ida_jni_doCommandNative_dispatcher.c
  • 行数:785
  • 字节:24373
  • SHA256:ed7edc81187e7531476ff2ad792cf8ceb5d6ae4c658564c8a5f15e4618140ff3

为什么看它:这是 Java native 调用进入 command worker 前的分发入口。

读完得到的结论:它证明 Java 侧 security command id 会进入 native 分发体系;后续 10405/10417/10418 才不是凭空猜出来的。

// JNI doCommandNative dispatcher. command is Java security command id; key sig3 ids: 10405 old sig3, 10417 default sig3, 10418 plus/alternate hash, 10412 init/router.
__int64 __fastcall JNI_doCommandNative_dispatcher(__int64 env, __int64 clazz, int command, __int64 args)
{
  char *v7; // x20
  int v8; // w24
  char *v9; // x27
  const char *v10; // x0
  const char *v11; // x22
  const char *v12; // x0
  const char *v13; // x22
  __int64 v14; // x24
  char *v15; // x21
  __int64 v16; // x0
  char *v17; // x20
  __int64 v18; // x22
  __int64 v19; // x0
  char *v20; // x21
  int v21; // w8
  char v22; // w0
  int v23; // w8
  int i; // w9
  const char *v25; // x0
  const char *v26; // x25
  const char *v27; // x0
  const char *v28; // x25
  int v29; // w8
  int j; // w9
  __int64 v32; // x0
  char v33; // w0
  __int64 *zt_table_manager_singleton; // x0
  int v35; // w8
  __int64 v36; // x23
  char v37; // w0
  int v38; // w8
  int v39; // w8
  int v40; // w9
  __int64 v41; // x28
  __int64 v42; // x0
  __int64 v43; // x28
  int v44; // w9
  __int64 v45; // x0
  __int64 v46; // x25
  char *v47; // x28
  int v48; // w9
  char *v49; // x23
  __int64 v50; // x0
  __int64 v51; // x23
  unsigned __int64 v52; // x25
  unsigned __int64 tv_usec; // x20
  size_t v54; // x2
  char *v55; // x0
  char *v56; // x1
  int v57; // w0
  int v58; // w8
  int v59; // w9
  int v60; // w8
  int v61; // w10
  int v62; // w9
  int v63; // w9
  int v64; // w8
  int v65; // w9
  __int64 v66; // x28
  __int64 v67; // x0
  __int64 v68; // x25
  __int64 v69; // x0
  __int64 v70; // x23
  int v71; // w8
  int v72; // w9
  int v73; // w9
  int v74; // w9
  __int64 v75; // x20
  __int64 v76; // x23
  __int64 v77; // x26
  __int64 v78; // x25
  __int64 v79; // x28
  __int64 v80; // x0
  __int64 v81; // x28
  __int64 v82; // x0
  __int64 *v83; // x0
  __int64 v84; // x0
  __int64 v85; // x25
  size_t v86; // [xsp+0h] [xbp-290h]
  size_t v87; // [xsp+0h] [xbp-290h]
  size_t v88; // [xsp+0h] [xbp-290h]
  size_t v89; // [xsp+0h] [xbp-290h]
  __int64 v90; // [xsp+0h] [xbp-290h]
  __int64 v91; // [xsp+0h] [xbp-290h]
  int v92; // [xsp+68h] [xbp-228h]
  __int64 v93; // [xsp+68h] [xbp-228h]
  int v94; // [xsp+80h] [xbp-210h]
  __int64 v95; // [xsp+A0h] [xbp-1F0h]
  __int64 v96; // [xsp+A8h] [xbp-1E8h]
  unsigned __int64 v97[2]; // [xsp+138h] [xbp-158h] BYREF
  void *v98; // [xsp+148h] [xbp-148h]
  void *v99; // [xsp+150h] [xbp-140h] BYREF
  __int64 v100; // [xsp+158h] [xbp-138h]
  __int64 v101; // [xsp+160h] [xbp-130h]
  __int16 v102; // [xsp+1A0h] [xbp-F0h]
  char v103; // [xsp+1A2h] [xbp-EEh]
  struct timeval tv; // [xsp+1E0h] [xbp-B0h] BYREF
  char *v105; // [xsp+1F0h] [xbp-A0h]

  _ReadStatusReg(TPIDR_EL0);
  v7 = &byte_70000;
  if ( (g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0 )
    goto LABEL_5;
  while ( 1 )
  {
    rand();
    gettimeofday(&tv, 0);
    v100 = 0;
    v101 = 0;
    v99 = 0;
    nullsub_8(123);
    v8 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 1368LL))(env, args);
    if ( g_opaque_predicate_a < 0 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 0x80000000) == 0 )
      break;
LABEL_5:
    rand();
    gettimeofday(&tv, 0);
    v100 = 0;
    v101 = 0;
    v99 = 0;
    nullsub_8(123);
    (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 1368LL))(env, args);
  }
  if ( v8 >= 7 )
  {
    v94 = command;
    while ( 1 )
    {
      nullsub_8(533);
      v18 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 4);
      v19 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
      v96 = v19;
      v20 = v19
          ? (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v19, 0)
          : 0LL;
      v87 = strlen(v20);
      std::string::assign((int)g_current_appkey, v20, v87);
      v21 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 10 || (v21 & 1) == 0 )
        break;
      v15 = v7;
      nullsub_8(533);
      (*(void (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 4);
      v16 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
      if ( v16 )
        v17 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v16, 0);
      else
        v17 = 0;
      v86 = strlen(v17);
      std::string::assign((int)g_current_appkey, v17, v86);
      v7 = v15;
    }
    if ( v94 == 10412 )                         // Command 10412 init/router path: initializes asset/context state and returns Java string "1" on success.
    {
      nullsub_8(866);
      tramp_security_router_init();
      v23 = g_opaque_predicate_a;
      for ( i = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
            g_opaque_predicate_a >= 10 && (i & 1) != 0;
            i = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773) )
      {
        nullsub_8(866);
        tramp_security_router_init();
        nullsub_8(866);
        tramp_security_router_init();
        v23 = g_opaque_predicate_a;
      }
      if ( (v22 & 1) != 0 )
      {
        if ( (v23 & 0x80000000) == 0 && i < 0 )
          goto LABEL_52;
        while ( 1 )
        {
          gettimeofday(&tv, 0);
          v32 = (*(__int64 (__fastcall **)(__int64, void *))(*(_QWORD *)env + 1336LL))(env, &unk_56499);
          v29 = g_opaque_predicate_a;
          v14 = v32;
          j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
          if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
            break;
LABEL_52:
          gettimeofday(&tv, 0);
          (*(void (__fastcall **)(__int64, void *))(*(_QWORD *)env + 1336LL))(env, &unk_56499);
        }
      }
      else
      {
        while ( 1 )
        {
          WORD2(tv.tv_sec) = -1424;
          LODWORD(tv.tv_sec) = 839400965;
          sub_3C54C();
          v28 = v27;
          sub_3B350(env, 0x11172u, v27);
          if ( v28 )
            sub_3C614();
          v29 = g_opaque_predicate_a;
          v14 = 0;
          j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
          if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
            break;
          WORD2(tv.tv_sec) = -1424;
          LODWORD(tv.tv_sec) = 839400965;
          sub_3C54C();
          v26 = v25;
          sub_3B350(env, 0x11172u, v25);
          if ( v26 )
            sub_3C614();
        }
      }
      goto LABEL_225;
    }
    if ( (g_opaque_predicate_a & 0x80000000) == 0 && v21 < 0 )
      goto LABEL_55;
    while ( 1 )
    {
      sub_3E5C0();
      v29 = g_opaque_predicate_a;
      j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 10 || (j & 1) == 0 )
        break;
LABEL_55:
      sub_3E5C0();
    }
    if ( (v33 & 1) != 0 )
    {
      if ( (g_sig3_feature_bits & 0x1800000000000LL) == 0 )
      {
        sub_3B350(env, 0x1117Cu, "70012");
        v29 = g_opaque_predicate_a;
        for ( j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
              (g_opaque_predicate_a & 0x80000000) == 0 && j < 0;
              j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773) )
        {
          sub_3B350(env, 0x1117Cu, "70012");
          sub_3B350(env, 0x1117Cu, "70012");
          v29 = g_opaque_predicate_a;
        }
      }
      if ( (v29 & 0x80000000) == 0 && j < 0 )
      {
        while ( 1 )
          ;
      }
      if ( (qword_70918 & 0x2000000000000000LL) == 0 )
      {
        if ( v29 >= 10 && (j & 1) != 0 )
          goto LABEL_101;
        while ( 1 )
        {
          sub_3B350(env, 0x111E5u, (const char *)&unk_59396);
          v29 = g_opaque_predicate_a;
          j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
          if ( g_opaque_predicate_a < 10 || (j & 1) == 0 )
            break;
LABEL_101:
          sub_3B350(env, 0x111E5u, (const char *)&unk_59396);
        }
      }
    }
    else
    {
      sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
      v29 = g_opaque_predicate_a;
      for ( j = *((_DWORD *)v7 + 773); g_opaque_predicate_a >= 10 && (((j - 1) * j) & 1) != 0; j = *((_DWORD *)v7 + 773) )
      {
        sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
        sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
        v29 = g_opaque_predicate_a;
      }
      LOBYTE(j) = (j - 1) * j;
      if ( v94 != 10411 )
      {
        v14 = 0;
        goto LABEL_225;
      }
    }
    if ( v29 >= 10 && (j & 1) != 0 )
      goto LABEL_81;
    while ( 1 )
    {
      zt_table_manager_singleton = get_zt_table_manager_singleton();
      v35 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 0 || (v35 & 0x80000000) == 0 )
        break;
LABEL_81:
      get_zt_table_manager_singleton();
    }
    if ( !*(_QWORD *)(zt_table_manager_singleton[1] + 8) )
    {
      if ( g_opaque_predicate_a >= 10 && (v35 & 1) != 0 )
        goto LABEL_104;
      while ( 1 )
      {
        sub_3B350(env, 0x11181u, (const char *)&unk_59396);
        v29 = g_opaque_predicate_a;
        v14 = 0;
        j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
        if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
          break;
LABEL_104:
        sub_3B350(env, 0x11181u, (const char *)&unk_59396);
      }
      goto LABEL_225;
    }
    while ( 1 )
    {
      v36 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1384LL))(env, args, 0);
      v37 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)env + 1824LL))(env);
      v38 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 0 || (v38 & 0x80000000) == 0 )
        break;
      (*(void (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1384LL))(env, args, 0);
      (*(void (__fastcall **)(__int64))(*(_QWORD *)env + 1824LL))(env);
    }
    if ( !v36 || v37 == 1 )
    {
      while ( 1 )
      {
        (*(void (__fastcall **)(__int64))(*(_QWORD *)env + 136LL))(env);
        sub_3B350(env, 0x11182u, (const char *)&unk_59396);
        v39 = g_opaque_predicate_a;
        v40 = (((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1;
        if ( g_opaque_predicate_a < 10
          || ((((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1) == 0 )
        {
          break;
        }
        (*(void (__fastcall **)(__int64))(*(_QWORD *)env + 136LL))(env);
        sub_3B350(env, 0x11182u, (const char *)&unk_59396);
      }
      v14 = 0;
      v41 = v36;
LABEL_217:
      if ( v39 >= 10 && v40 && v41 )
      {
        (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v41);
        v41 = 0;
      }
      do
      {
        if ( v41 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v41);
        v29 = g_opaque_predicate_a;
        j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
        if ( g_opaque_predicate_a < 10 )
          break;
        v41 = 0;
      }
      while ( (j & 1) != 0 );
LABEL_225:
      v9 = v7;
      if ( v29 < 10 )
      {
        v77 = v96;
        v76 = v96;
        v79 = v96;
        if ( !v20 )
          goto LABEL_235;
        goto LABEL_234;
      }
      v75 = v96;
      v76 = v96;
      v77 = v96;
      v78 = v96;
      v79 = v96;
      if ( (j & 1) == 0 )
        goto LABEL_233;
      if ( v20 )
      {
LABEL_228:
        (*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v79, v20);
        goto LABEL_229;
      }
      while ( 1 )
      {
LABEL_229:
        if ( v75 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v78);
        if ( v18 )
        {
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v18);
          v76 = 0;
          v77 = 0;
          v18 = 0;
          if ( v20 )
            goto LABEL_234;
        }
        else
        {
          v76 = 0;
          v77 = 0;
LABEL_233:
          if ( v20 )
LABEL_234:
            (*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v79, v20);
        }
LABEL_235:
        if ( v77 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v76);
        if ( v18 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v18);
        if ( g_opaque_predicate_a < 0 || (((*((_DWORD *)v9 + 773) - 1) * *((_DWORD *)v9 + 773)) & 0x80000000) == 0 )
          goto LABEL_45;
        v78 = 0;
        v75 = 0;
        v18 = 0;
        if ( v20 )
          goto LABEL_228;
      }
    }
    if ( g_opaque_predicate_a >= 10 && (v38 & 1) != 0 )
      goto LABEL_112;
    while ( 1 )
    {
      gettimeofday(&tv, 0);
      v42 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
      if ( v36 == v42 )
      {
        v43 = v36;
      }
      else
      {
        v43 = v42;
        if ( v36 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v36);
      }
      std_string_from_cstr(v97, (char *)&unk_59396);
      v44 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 10 || (v44 & 1) == 0 )
        break;
      v36 = v43;
LABEL_112:
      gettimeofday(&tv, 0);
      v45 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
      if ( v36 == v45 )
      {
        v46 = v36;
      }
      else
      {
        v46 = v45;
        if ( v36 )
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v36);
      }
      std_string_from_cstr(v97, (char *)&unk_59396);
      v36 = v46;
    }
    if ( !v43 )
    {
      if ( g_opaque_predicate_a >= 0 && v44 < 0 )
        goto LABEL_255;
      while ( 1 )
      {
        sub_3B350(env, 0x11185u, (const char *)&unk_59396);
        v64 = g_opaque_predicate_a;
        v41 = 0;
        v14 = 0;
        v65 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
        if ( g_opaque_predicate_a < 10 || (v65 & 1) == 0 )
          break;
LABEL_255:
        sub_3B350(env, 0x11185u, (const char *)&unk_59396);
      }
      goto LABEL_208;
    }
    v95 = v43;
    if ( g_opaque_predicate_a >= 0 && v44 < 0 )
      goto LABEL_124;
    while ( 1 )
    {
      v47 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v95, 0);
      v88 = strlen(v47);
      std::string::assign((int)v97, v47, v88);
      v48 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 10 || (v48 & 1) == 0 )
        break;
LABEL_124:
      v49 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v95, 0);
      v89 = strlen(v49);
      std::string::assign((int)v97, v49, v89);
    }
    if ( !*v47 )
    {
      if ( g_opaque_predicate_a >= 0 && v48 < 0 )
        goto LABEL_181;
      while ( 1 )
      {
        sub_3B350(env, 0x11184u, (const char *)&unk_59396);
        v60 = g_opaque_predicate_a;
        v63 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
        if ( g_opaque_predicate_a < 0 || (v63 & 0x80000000) == 0 )
          break;
LABEL_181:
        sub_3B350(env, 0x11184u, (const char *)&unk_59396);
      }
      v62 = v63 & 1;
      v92 = 1;
      goto LABEL_183;
    }
    if ( g_opaque_predicate_a >= 0 && v48 < 0 )
      goto LABEL_152;
    while ( 1 )
    {
      v103 = 11;
      v102 = 32514;
      sub_3C54C();
      v51 = v50;
      get_zt_table_manager_singleton();
      sub_2CCFC();
      if ( (v97[0] & 1) != 0 )
        v52 = v97[1];
      else
        v52 = (unsigned __int64)LOBYTE(v97[0]) >> 1;
      if ( (tv.tv_sec & 1) != 0 )
        tv_usec = tv.tv_usec;
      else
        tv_usec = (unsigned __int64)LOBYTE(tv.tv_sec) >> 1;
      if ( v52 >= tv_usec )
        v54 = tv_usec;
      else
        v54 = v52;
      if ( !v54
        || ((tv.tv_sec & 1) != 0 ? (v55 = v105) : (v55 = (char *)&tv.tv_sec + 1),
            (v97[0] & 1) != 0 ? (v56 = (char *)v98) : (v56 = (char *)v97 + 1),
            (v57 = memcmp(v55, v56, v54)) == 0) )
      {
        if ( tv_usec < v52 )
          v57 = -1;
        else
          v57 = v52 < tv_usec;
      }
      v58 = g_opaque_predicate_a;
      v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
      if ( g_opaque_predicate_a < 0 || (v59 & 0x80000000) == 0 )
        break;
LABEL_152:
      v103 = 11;
      v102 = 32514;
      sub_3C54C();
      get_zt_table_manager_singleton();
      sub_2CCFC();
    }
    if ( v57 )
    {
      sub_3B350(env, 0x11183u, (const char *)&unk_59396);
      v58 = g_opaque_predicate_a;
      v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
      v92 = 1;
      if ( (g_opaque_predicate_a & 0x80000000) == 0 )
      {
        v7 = &byte_70000;
        do
        {
          if ( (v59 & 0x80000000) == 0 )
            break;
          sub_3B350(env, 0x11183u, (const char *)&unk_59396);
          sub_3B350(env, 0x11183u, (const char *)&unk_59396);
          v58 = g_opaque_predicate_a;
          v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
          v92 = 1;
        }
        while ( (g_opaque_predicate_a & 0x80000000) == 0 );
        goto LABEL_161;
      }
    }
    else
    {
      v92 = 0;
    }
    v7 = &byte_70000;
LABEL_161:
    if ( v58 >= 10 && (v59 & 1) != 0 )
      goto LABEL_170;
    while ( 1 )
    {
      if ( (tv.tv_sec & 1) != 0 )
        operator delete(v105);
      if ( v51 )
        sub_3C614();
      v60 = g_opaque_predicate_a;
      v61 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
      v62 = v61 & 1;
      if ( g_opaque_predicate_a < 10 || (v61 & 1) == 0 )
        break;
      v51 = 0;
LABEL_170:
      if ( (tv.tv_sec & 1) != 0 )
        operator delete(v105);
      if ( v51 )
        sub_3C614();
      v51 = 0;
    }
    if ( !v92 )
    {
      v92 = 0;
      if ( (g_opaque_predicate_a & 0x80000000) == 0 && v61 < 0 )
      {
        while ( 1 )
          ;
      }
    }
LABEL_183:
    if ( v60 >= 10 && v62 )
      goto LABEL_189;
LABEL_185:
    if ( v47 )
      (*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v95, v47);
    while ( 1 )
    {
      v64 = g_opaque_predicate_a;
      v65 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 0 || (v65 & 0x80000000) == 0 )
        break;
LABEL_189:
      if ( v47 )
      {
        (*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v95, v47);
        goto LABEL_185;
      }
    }
    if ( !v92 )
    {
      v66 = v95;
      while ( 1 )
      {
        gettimeofday(&tv, 0);
        v69 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 2);
        if ( v66 == v69 )
        {
          v70 = v66;
        }
        else
        {
          v70 = v69;
          if ( v66 )
            (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v66);
        }
        v71 = g_opaque_predicate_a;
        v72 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
        if ( g_opaque_predicate_a < 0 || (v72 & 0x80000000) == 0 )
          break;
        gettimeofday(&tv, 0);
        v67 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 2);
        v66 = v70;
        if ( v70 != v67 )
        {
          v68 = v67;
          v66 = v67;
          if ( v70 )
          {
            (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
            v66 = v68;
          }
        }
      }
      v73 = v72 & 1;
      if ( v70 )
      {
        if ( g_opaque_predicate_a >= 10 && v73 )
          goto LABEL_249;
        while ( 1 )
        {
          sub_386DC(env, v70);
          v71 = g_opaque_predicate_a;
          v73 = (((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1;
          if ( g_opaque_predicate_a < 10
            || ((((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1) == 0 )
          {
            break;
          }
LABEL_249:
          sub_386DC(env, v70);
        }
      }
      if ( v71 >= 10 && v73 )
        goto LABEL_263;
      while ( 1 )
      {
        v80 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
        if ( v70 == v80 )
        {
          v81 = v70;
        }
        else
        {
          v81 = v80;
          if ( v70 )
            (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
        }
        v82 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
        sub_38474(env, v82);
        v93 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 6);
        sub_38474(env, v93);
        v83 = get_zt_table_manager_singleton();
        if ( g_opaque_predicate_a < 10 || (((*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773)) & 1) == 0 )
        {
          if ( v83[3] )
          {
            while ( 1 )
            {
              get_zt_table_manager_singleton();
              if ( g_opaque_predicate_a < 0 || (((*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773)) & 0x80000000) == 0 )
                break;
              get_zt_table_manager_singleton();
            }
          }
          JUMPOUT(0x42148);
        }
        v70 = v81;
LABEL_263:
        v84 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
        if ( v70 == v84 )
        {
          v85 = v70;
        }
        else
        {
          v85 = v84;
          if ( v70 )
            (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
        }
        v90 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
        sub_38474(env, v90);
        v91 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 6);
        sub_38474(env, v91);
        get_zt_table_manager_singleton();
        v70 = v85;
      }
    }
    v41 = v95;
    v14 = 0;
LABEL_208:
    if ( (v64 & 0x80000000) == 0 && v65 < 0 )
      goto LABEL_214;
    while ( 1 )
    {
      if ( (v97[0] & 1) != 0 )
        operator delete(v98);
      v39 = g_opaque_predicate_a;
      v74 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
      if ( g_opaque_predicate_a < 0 || (v74 & 0x80000000) == 0 )
        break;
LABEL_214:
      if ( (v97[0] & 1) != 0 )
        operator delete(v98);
    }
    v40 = v74 & 1;
    goto LABEL_217;
  }
  v9 = &byte_70000;
  while ( 1 )
  {
    nullsub_8(111);
    WORD2(tv.tv_sec) = -656;
    LODWORD(tv.tv_sec) = 839400965;
    sub_3C54C();
    v13 = v12;
    sub_3B350(env, 0x11171u, v12);
    if ( v13 )
      sub_3C614();
    v14 = 0;
    if ( g_opaque_predicate_a < 0 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 0x80000000) == 0 )
      break;
    nullsub_8(111);
    WORD2(tv.tv_sec) = -656;
    LODWORD(tv.tv_sec) = 839400965;
    sub_3C54C();
    v11 = v10;
    sub_3B350(env, 0x11171u, v10);
    if ( v11 )
      sub_3C614();
  }
LABEL_45:
  sub_DCE8(&v99);
  while ( (g_opaque_predicate_a & 0x80000000) == 0 && (*((_DWORD *)v9 + 773) - 1) * *((_DWORD *)v9 + 773) < 0 )
  {
    sub_DCE8(&v99);
    sub_DCE8(&v99);
  }
  return v14;
}

核心 command worker:ida_sg_command_dispatch_body.c

  • 文件:output/ida_sg_command_dispatch_body.c
  • 行数:1187
  • 字节:39388
  • SHA256:dc049f909f85d4584f3834406bc1fe7152ce2661b781a621957bfd40b5b43130

为什么看它:这是恢复 __NS_sig3 的核心伪代码。完整分支里能看到 case 10417case 10418default10405

读完得到的结论:Python 的三种模式直接对应这里的三个 native command:sig3_10405()sig3_10417()sig3_10418()

// Central command worker reached through trampoline. Dispatches 10405/10417/10418 and crypto command families.
void __usercall sg_command_dispatch_body(int a2@<W2>, unsigned __int8 *a3@<X8>, ...)
{
  int v4; // w11
  char *v5; // x22
  __int64 *stack; // x8
  __int64 v7; // x24
  __int64 *v8; // x19
  __int64 v9; // x0
  __int64 v10; // x26
  __int64 v11; // x0
  __int64 v12; // x20
  void *v13; // x21
  int v14; // w8
  int i; // w9
  int v16; // w10
  signed __int64 v17; // x8
  void **v18; // x0
  char *v19; // x19
  char *v20; // x0
  char *v21; // x23
  void *v22; // x23
  char *v23; // x26
  char v24; // w28
  signed __int64 v25; // x8
  void **v26; // x27
  void **v27; // x19
  char *v28; // x22
  void **v29; // x0
  char *v30; // x0
  char *v31; // x25
  int v32; // w8
  int v33; // w9
  unsigned int v34; // w27
  void **v35; // x1
  void **v36; // x1
  int v37; // w8
  int v38; // w9
  unsigned __int64 v39; // x10
  unsigned __int64 v40; // x10
  int v41; // w22
  unsigned __int64 v42; // x11
  int v43; // w10
  unsigned __int64 v44; // x11
  int v45; // w24
  __int64 v46; // x0
  __int64 v47; // x24
  __int64 v48; // x0
  __int64 v49; // x24
  unsigned int v50; // w8
  int v51; // w8
  unsigned int v52; // w9
  int v53; // w9
  int v54; // w9
  __int64 v55; // x1
  __int64 v56; // x1
  char v57; // w0
  int v58; // w8
  __int64 v59; // x0
  __int64 v60; // x22
  int v61; // w9
  char v62; // w9
  int v63; // w8
  char v64; // w9
  int v65; // w9
  int v66; // w8
  int v67; // w9
  int v68; // w9
  char v69; // w0
  int v70; // w8
  char v71; // w9
  int v72; // w8
  char v73; // w9
  char v74; // w9
  int v75; // w8
  char v76; // w9
  __int64 v77; // x1
  char v78; // w0
  int v79; // w9
  __int64 v80; // x1
  _BOOL4 v81; // w8
  int v82; // w9
  int v83; // w8
  int v84; // w9
  __int64 v85; // x0
  void *v86; // x23
  int v87; // w8
  char *v88; // x0
  void *v89; // x0
  int v90; // w8
  int v91; // w8
  int v92; // w9
  __int128 v93; // q0
  __int128 v94; // q0
  int v95; // w9
  __int64 v96; // x0
  __int64 v97; // x22
  __int64 v98; // x0
  __int64 v99; // x22
  void **v100; // x1
  __int128 v101; // q0
  int v102; // w9
  char *v103; // x1
  _BYTE *v104; // x8
  __int128 v105; // q0
  unsigned __int8 *v106; // x9
  unsigned __int64 v107; // x8
  int v108; // t1
  char v109; // w8
  unsigned __int8 *v110; // x19
  __int64 *v111; // x0
  void **v112; // x2
  __int64 *zt_table_manager_singleton; // x0
  char *v114; // x2
  int v115; // w8
  int v116; // w9
  int v117; // w23
  __int64 v118; // x0
  __int64 v119; // x0
  int v120; // w8
  int v121; // w9
  unsigned __int64 v122; // x10
  _BYTE *v123; // x10
  __int128 v124; // q0
  _BYTE *v125; // x10
  __int128 v126; // q0
  size_t n; // [xsp+D8h] [xbp-E8h] BYREF
  __int128 hex_string; // [xsp+E0h] [xbp-E0h] BYREF
  void *v130; // [xsp+F0h] [xbp-D0h]
  void *v131; // [xsp+F8h] [xbp-C8h] BYREF
  unsigned __int64 v132; // [xsp+100h] [xbp-C0h]
  void **v133; // [xsp+108h] [xbp-B8h]
  __int128 v134; // [xsp+110h] [xbp-B0h] BYREF
  _BYTE *v135; // [xsp+120h] [xbp-A0h]
  gcc_va_list va; // [xsp+128h] [xbp-98h] BYREF
  __int128 v137; // [xsp+148h] [xbp-78h] BYREF
  void *v138; // [xsp+158h] [xbp-68h]
  __int64 v139; // [xsp+160h] [xbp-60h]

  v139 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
  if ( g_opaque_predicate_a >= 10 && (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) != 0 )
    goto LABEL_5;
  while ( 1 )
  {
    va_end(va);
    va_start(va, a3);
    v4 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
    if ( g_opaque_predicate_a < 0 || (v4 & 0x80000000) == 0 )
    {
      if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
        va[0].__gr_offs = -32;
      va[0].__gr_offs = -32;
      if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
      {
        while ( 1 )
          ;
      }
      v5 = (char *)*((_QWORD *)va[0].__gr_top - 5);
      va[0].__gr_offs = -24;
      if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
      {
        while ( 1 )
          ;
      }
      va[0].__gr_offs = 0;
      v34 = *((_BYTE *)va[0].__gr_top - 8) & 1;
      stack = (__int64 *)va[0].__stack;
      va[0].__stack = (char *)va[0].__stack + 8;
      v7 = *stack;
      v8 = (__int64 *)va[0].__stack;
      va[0].__stack = (char *)va[0].__stack + 8;
      while ( 1 )
      {
        v10 = *v8;
        v135 = 0;
        v134 = 0u;
        sub_3C54C();
        v12 = v11;
        v13 = (void *)operator new(1u);
        sub_1F404(v13, v34);
        v14 = g_opaque_predicate_a;
        i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
        v16 = i & 1;
        if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
          break;
        v135 = 0;
        v134 = 0u;
        sub_3C54C();
        v9 = operator new(1u);
        sub_1F404(v9, v34);
      }
      switch ( a2 )
      {
        case 10411:
          while ( 1 )
          {
            v22 = (void *)operator new(1u);
            v23 = v5;
            std_string_from_cstr((unsigned __int64 *)&v131, v5);
            v24 = (char)v131;
            v25 = v132;
            v26 = v133;
            if ( ((unsigned __int8)v131 & 1) != 0 )
              v27 = v133;
            else
              v27 = (void **)((char *)&v131 + 1);
            if ( ((unsigned __int8)v131 & 1) == 0 )
              v25 = (unsigned __int64)(unsigned __int8)v131 >> 1;
            v28 = (char *)v27 + v25;
            if ( v25 >= 6 )
            {
              v29 = v27;
              do
              {
                v30 = (char *)memchr(v29, 114, v25 - 5);
                if ( !v30 )
                  break;
                v31 = v30;
                if ( !memcmp(v30, "result", 6u) )
                  goto LABEL_36;
                v29 = (void **)(v31 + 1);
                v25 = v28 - (v31 + 1);
              }
              while ( v25 > 5 );
            }
            v31 = v28;
LABEL_36:
            v32 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            if ( g_opaque_predicate_a < 10 || (v32 & 1) == 0 )
              break;
            v5 = v23;
            std_string_from_cstr((unsigned __int64 *)&v131, v23);
            v17 = (unsigned __int64)(unsigned __int8)v131 >> 1;
            if ( ((unsigned __int8)v131 & 1) != 0 )
            {
              v17 = v132;
              v18 = v133;
            }
            else
            {
              v18 = (void **)((char *)&v131 + 1);
            }
            if ( v17 >= 6 )
            {
              v19 = (char *)v18 + v17;
              do
              {
                v20 = (char *)memchr(v18, 114, v17 - 5);
                if ( !v20 )
                  break;
                v21 = v20;
                if ( !memcmp(v20, "result", 6u) )
                  break;
                v18 = (void **)(v21 + 1);
                v17 = v19 - (v21 + 1);
              }
              while ( v17 >= 6 );
            }
          }
          if ( v31 == v28 || v31 - (char *)v27 == -1 )
          {
            while ( 1 )
            {
              v36 = (v24 & 1) != 0 ? v26 : (void **)((char *)&v131 + 1);
              sub_1AD30(&hex_string, v22, v36);
              if ( (v134 & 1) != 0 )
              {
                *v135 = 0;
                *((_QWORD *)&v134 + 1) = 0;
                if ( (v134 & 1) != 0 )
                {
                  operator delete(v135);
                  *(_QWORD *)&v134 = 0;
                }
              }
              else
              {
                LOWORD(v134) = 0;
              }
              v37 = g_opaque_predicate_a;
              v135 = v130;
              v38 = (((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1;
              v134 = hex_string;
              if ( g_opaque_predicate_a < 10
                || ((((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1) == 0 )
              {
                break;
              }
              if ( ((unsigned __int8)v131 & 1) != 0 )
                v35 = v133;
              else
                v35 = (void **)((char *)&v131 + 1);
              sub_1AD30(&hex_string, v22, v35);
              if ( (v134 & 1) != 0 )
              {
                *v135 = 0;
                *((_QWORD *)&v134 + 1) = 0;
                if ( (v134 & 1) != 0 )
                {
                  operator delete(v135);
                  *(_QWORD *)&v134 = 0;
                }
              }
              else
              {
                LOWORD(v134) = 0;
              }
              v24 = (char)v131;
              v26 = v133;
              v135 = v130;
              v134 = hex_string;
            }
          }
          else
          {
            v33 = v24 & 1;
            if ( (g_opaque_predicate_a & 0x80000000) == 0 && v32 < 0 )
              goto LABEL_268;
            while ( 1 )
            {
              v100 = v33 ? v26 : (void **)((char *)&v131 + 1);
              sub_1B3B8(&hex_string, v22, v100);
              if ( (v134 & 1) != 0 )
              {
                *v135 = 0;
                *((_QWORD *)&v134 + 1) = 0;
                if ( (v134 & 1) != 0 )
                {
                  operator delete(v135);
                  *(_QWORD *)&v134 = 0;
                }
              }
              else
              {
                LOWORD(v134) = 0;
              }
              v101 = hex_string;
              v37 = g_opaque_predicate_a;
              v135 = v130;
              v102 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              hex_string = 0u;
              v130 = 0;
              v134 = v101;
              if ( g_opaque_predicate_a < 0 || (v102 & 0x80000000) == 0 )
                break;
              v24 = (char)v131;
              v26 = v133;
LABEL_268:
              if ( (v24 & 1) != 0 )
                v103 = (char *)v26;
              else
                v103 = (char *)&v131 + 1;
              sub_1B3B8(&hex_string, v22, v103);
              if ( (v134 & 1) != 0 )
              {
                *v135 = 0;
                *((_QWORD *)&v134 + 1) = 0;
                if ( (v134 & 1) != 0 )
                {
                  operator delete(v135);
                  *(_QWORD *)&v134 = 0;
                }
              }
              else
              {
                LOWORD(v134) = 0;
              }
              v104 = v130;
              v105 = hex_string;
              v26 = v133;
              hex_string = 0u;
              v130 = 0;
              v135 = v104;
              v134 = v105;
              v33 = (unsigned __int8)v131 & 1;
            }
            v38 = v102 & 1;
          }
          if ( v37 >= 10 && v38 )
            goto LABEL_286;
          while ( 1 )
          {
            if ( ((unsigned __int8)v131 & 1) != 0 )
              operator delete(v133);
            if ( v22 )
              operator delete(v22);
            v14 = g_opaque_predicate_a;
            i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            v16 = i & 1;
            if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
              break;
            v22 = 0;
LABEL_286:
            if ( ((unsigned __int8)v131 & 1) != 0 )
              operator delete(v133);
            if ( v22 )
            {
              operator delete(v22);
              v22 = 0;
            }
          }
          goto LABEL_378;
        case 10412:
        case 10414:
        case 10415:
        case 10416:
          goto LABEL_378;
        case 10413:
          if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
            goto LABEL_290;
          do
          {
            std_string_from_cstr((unsigned __int64 *)&v131, v5);
LABEL_290:
            std_string_from_cstr((unsigned __int64 *)&v131, v5);
            v106 = (unsigned __int8 *)v133;
            if ( ((unsigned __int8)v131 & 1) != 0 )
              v107 = v132;
            else
              v107 = (unsigned __int64)(unsigned __int8)v131 >> 1;
            if ( ((unsigned __int8)v131 & 1) == 0 )
              v106 = (unsigned __int8 *)&v131 + 1;
            if ( v107 )
            {
              while ( 1 )
              {
                v108 = *v106++;
                if ( (unsigned int)(v108 - 48) >= 0xA )
                  break;
                if ( !--v107 )
                  goto LABEL_298;
              }
              v109 = 0;
            }
            else
            {
LABEL_298:
              v109 = 1;
            }
          }
          while ( (g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0 );
          if ( (v109 & 1) != 0 )
          {
            sub_DBD4((int)&v131, 0, "p", 1u);
            v130 = 0;
            for ( hex_string = 0u;
                  (g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0;
                  hex_string = 0u )
            {
              sub_DBD4((int)&v131, 0, "p", 1u);
              sub_DBD4((int)&v131, 0, "p", 1u);
              v130 = 0;
            }
            if ( (v34 & 1) != 0 )
            {
              v110 = a3;
              while ( 1 )
              {
                zt_table_manager_singleton = get_zt_table_manager_singleton();
                v114 = ((unsigned __int8)v131 & 1) != 0 ? (char *)v133 : (char *)&v131 + 1;
                sub_2B248(&v137, zt_table_manager_singleton, 3, v114, v7, v10);
                if ( (hex_string & 1) != 0 )
                {
                  *(_BYTE *)v130 = 0;
                  *((_QWORD *)&hex_string + 1) = 0;
                  if ( (hex_string & 1) != 0 )
                  {
                    operator delete(v130);
                    *(_QWORD *)&hex_string = 0;
                  }
                }
                else
                {
                  LOWORD(hex_string) = 0;
                }
                v115 = g_opaque_predicate_a;
                v130 = v138;
                v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                hex_string = v137;
                if ( g_opaque_predicate_a < 10 || (v116 & 1) == 0 )
                  break;
                v111 = get_zt_table_manager_singleton();
                if ( ((unsigned __int8)v131 & 1) != 0 )
                  v112 = v133;
                else
                  v112 = (void **)((char *)&v131 + 1);
                sub_2B248(&v137, v111, 3, v112, v7, v10);
                if ( (hex_string & 1) != 0 )
                {
                  *(_BYTE *)v130 = 0;
                  *((_QWORD *)&hex_string + 1) = 0;
                  if ( (hex_string & 1) != 0 )
                  {
                    operator delete(v130);
                    *(_QWORD *)&hex_string = 0;
                  }
                }
                else
                {
                  LOWORD(hex_string) = 0;
                }
                v130 = v138;
                hex_string = v137;
              }
            }
            else
            {
              v110 = a3;
              while ( 1 )
              {
                get_zt_table_manager_singleton();
                sub_2CCFC();
                if ( (hex_string & 1) != 0 )
                {
                  *(_BYTE *)v130 = 0;
                  *((_QWORD *)&hex_string + 1) = 0;
                  if ( (hex_string & 1) != 0 )
                  {
                    operator delete(v130);
                    *(_QWORD *)&hex_string = 0;
                  }
                }
                else
                {
                  LOWORD(hex_string) = 0;
                }
                v115 = g_opaque_predicate_a;
                v130 = v138;
                v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                hex_string = v137;
                if ( g_opaque_predicate_a < 10 || (v116 & 1) == 0 )
                  break;
                get_zt_table_manager_singleton();
                sub_2CCFC();
                if ( (hex_string & 1) != 0 )
                {
                  *(_BYTE *)v130 = 0;
                  *((_QWORD *)&hex_string + 1) = 0;
                  if ( (hex_string & 1) != 0 )
                  {
                    operator delete(v130);
                    *(_QWORD *)&hex_string = 0;
                  }
                }
                else
                {
                  LOWORD(hex_string) = 0;
                }
                v130 = v138;
                hex_string = v137;
              }
            }
            if ( v115 >= 0 && v116 < 0 )
            {
              while ( 1 )
                ;
            }
            if ( (hex_string & 1) != 0 )
              v122 = *((_QWORD *)&hex_string + 1);
            else
              v122 = (unsigned __int64)(unsigned __int8)hex_string >> 1;
            if ( v122 )
            {
              fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
              v115 = g_opaque_predicate_a;
              v117 = 2;
              v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( (g_opaque_predicate_a & 0x80000000) == 0 && v116 < 0 )
              {
                v117 = 2;
                do
                {
                  fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
                  fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
                  v115 = g_opaque_predicate_a;
                  v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                }
                while ( (g_opaque_predicate_a & 0x80000000) == 0 && v116 < 0 );
              }
            }
            else
            {
              v117 = 1;
              *(_QWORD *)v110 = 0;
              *((_QWORD *)v110 + 1) = 0;
              *((_QWORD *)v110 + 2) = 0;
            }
            if ( v115 >= 10 && (v116 & 1) != 0 )
              goto LABEL_367;
            while ( 1 )
            {
              if ( (hex_string & 1) != 0 )
                operator delete(v130);
              v120 = g_opaque_predicate_a;
              v121 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 10 || (v121 & 1) == 0 )
                break;
LABEL_367:
              if ( (hex_string & 1) != 0 )
                operator delete(v130);
            }
          }
          else
          {
            v117 = 1;
            while ( 1 )
            {
              sub_3C54C();
              *((_QWORD *)a3 + 1) = 0;
              *((_QWORD *)a3 + 2) = 0;
              *(_QWORD *)a3 = 0;
              if ( v119 )
                sub_3C614();
              v120 = g_opaque_predicate_a;
              v121 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 0 || (v121 & 0x80000000) == 0 )
                break;
              sub_3C54C();
              *((_QWORD *)a3 + 1) = 0;
              *((_QWORD *)a3 + 2) = 0;
              *(_QWORD *)a3 = 0;
              if ( v118 )
                sub_3C614();
            }
          }
          if ( (v120 & 0x80000000) == 0 && v121 < 0 )
            goto LABEL_375;
          while ( 1 )
          {
            if ( ((unsigned __int8)v131 & 1) != 0 )
              operator delete(v133);
            v14 = g_opaque_predicate_a;
            i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            v16 = i & 1;
            if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
              break;
LABEL_375:
            if ( ((unsigned __int8)v131 & 1) != 0 )
              operator delete(v133);
          }
          if ( v117 == 2 )
            goto LABEL_378;
          goto LABEL_383;
        case 10417:
          if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
            goto LABEL_117;
          do
          {
            do
              v52 = __ldaxr((unsigned int *)&dword_70430);
            while ( __stlxr(v52 + 1, (unsigned int *)&dword_70430) );
            do
LABEL_117:
              v50 = __ldaxr((unsigned int *)&dword_70430);
            while ( __stlxr(v50 + 1, (unsigned int *)&dword_70430) );
            v51 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
          }
          while ( g_opaque_predicate_a >= 10 && (v51 & 1) != 0 );
          if ( !v5 )
            goto LABEL_127;
          if ( (g_opaque_predicate_a & 0x80000000) == 0 && v51 < 0 )
          {
            while ( 1 )
              ;
          }
          if ( *v5 )
          {
            while ( 1 )
            {
              std_string_from_cstr((unsigned __int64 *)&hex_string, v5);
              hex_string_to_bytes_vector(&hex_string, v56);
              if ( (hex_string & 1) != 0 )
                operator delete(v130);
              v137 = 0u;
              sub_203F4(&v137, v131);
              v57 = sub_203FC(&v137);
              v58 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 0 || (v58 & 0x80000000) == 0 )
                break;
              std_string_from_cstr((unsigned __int64 *)&hex_string, v5);
              hex_string_to_bytes_vector(&hex_string, v55);
              if ( (hex_string & 1) != 0 )
                operator delete(v130);
              v137 = 0u;
              sub_203F4(&v137, v131);
              sub_203FC(&v137);
            }
            if ( (v57 & 1) != 0 )
            {
              while ( 1 )
              {
                v59 = sub_C0C4(&v137);
                v60 = v59;
                v61 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                if ( g_opaque_predicate_a < 10 || (v61 & 1) == 0 )
                  break;
                sub_C0C4(&v137);
              }
              if ( v59 )
              {
                if ( g_opaque_predicate_a < 0 || v61 >= 0 )
                  goto LABEL_165;
                do
                {
                  (*(void (__fastcall **)(__int64))(*(_QWORD *)v60 + 16LL))(v60);
LABEL_165:
                  v69 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v60 + 16LL))(v60);
                  v70 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                }
                while ( (g_opaque_predicate_a & 0x80000000) == 0 && v70 < 0 );
                if ( (v69 & 1) != 0 )
                {
                  if ( g_opaque_predicate_a >= 10 && (v70 & 1) != 0 )
                    goto LABEL_179;
                  while ( 1 )
                  {
                    v77 = atomic_load((unsigned int *)&dword_70430);
                    v78 = sub_1FB44(v60, v77);
                    v79 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                    if ( g_opaque_predicate_a < 0 || (v79 & 0x80000000) == 0 )
                      break;
LABEL_179:
                    v80 = atomic_load((unsigned int *)&dword_70430);
                    sub_1FB44(v60, v80);
                  }
                  v81 = g_opaque_predicate_a < 10 || (v79 & 1) == 0;
                  if ( (v78 & 1) != 0 )
                  {
                    if ( v81 )
                      goto LABEL_187;
                    do
                    {
                      n = 0;
                      sub_1FDAC(v60, &n);
LABEL_187:
                      n = 0;
                      v85 = sub_1FDAC(v60, &n);
                      v86 = (void *)v85;
                      v87 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                    }
                    while ( (g_opaque_predicate_a & 0x80000000) == 0 && v87 < 0 );
                    if ( v85 )
                    {
                      while ( 1 )
                      {
                        v130 = 0;
                        hex_string = 0u;
                        std::string::resize(&hex_string, n, 0);
                        v89 = (hex_string & 1) != 0 ? v130 : (char *)&hex_string + 1;
                        memcpy(v89, v86, n);
                        v90 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                        if ( g_opaque_predicate_a < 0 || (v90 & 0x80000000) == 0 )
                          break;
                        v130 = 0;
                        hex_string = 0u;
                        std::string::resize(&hex_string, n, 0);
                        if ( (hex_string & 1) != 0 )
                          v88 = (char *)v130;
                        else
                          v88 = (char *)&hex_string + 1;
                        memcpy(v88, v86, n);
                      }
                      if ( g_opaque_predicate_a >= 10 && (v90 & 1) != 0 )
                        goto LABEL_209;
                      while ( 1 )
                      {
                        operator delete(v86);
                        v91 = g_opaque_predicate_a;
                        v92 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
                        if ( g_opaque_predicate_a < 10 || (v92 & 1) == 0 )
                          break;
LABEL_209:
                        operator delete(v86);
                      }
                      if ( (g_opaque_predicate_a & 0x80000000) == 0 && v92 < 0 )
                        goto LABEL_214;
                      while ( 1 )
                      {
                        v93 = hex_string;
                        *((_QWORD *)a3 + 2) = v130;
                        *(_OWORD *)a3 = v93;
                        if ( v91 < 0 || (v92 & 0x80000000) == 0 )
                          break;
LABEL_214:
                        v94 = hex_string;
                        *((_QWORD *)a3 + 2) = v130;
                        *(_OWORD *)a3 = v94;
                      }
                    }
                    else
                    {
                      if ( g_opaque_predicate_a >= 10 && (v87 & 1) != 0 )
                        goto LABEL_217;
                      while ( 1 )
                      {
                        sub_3B7B4(10002205, &unk_59396);
                        v95 = g_opaque_predicate_b;
                        v91 = g_opaque_predicate_a;
                        *((_QWORD *)a3 + 1) = 0;
                        *((_QWORD *)a3 + 2) = 0;
                        *(_QWORD *)a3 = 0;
                        v92 = (v95 - 1) * v95;
                        if ( v91 < 0 || (v92 & 0x80000000) == 0 )
                          break;
LABEL_217:
                        sub_3B7B4(10002205, &unk_59396);
                        *((_QWORD *)a3 + 1) = 0;
                        *((_QWORD *)a3 + 2) = 0;
                        *(_QWORD *)a3 = 0;
                      }
                    }
                    if ( v91 >= 10 && (v92 & 1) != 0 )
                    {
                      while ( 1 )
                        ;
                    }
                  }
                  else
                  {
                    if ( !v81 )
                      goto LABEL_186;
                    while ( 1 )
                    {
                      sub_3B7B4(10002204, &unk_59396);
                      v82 = g_opaque_predicate_b;
                      v83 = g_opaque_predicate_a;
                      *((_QWORD *)a3 + 1) = 0;
                      *((_QWORD *)a3 + 2) = 0;
                      *(_QWORD *)a3 = 0;
                      v84 = (v82 - 1) * v82;
                      if ( v83 < 0 || (v84 & 0x80000000) == 0 )
                        break;
LABEL_186:
                      sub_3B7B4(10002204, &unk_59396);
                      *((_QWORD *)a3 + 1) = 0;
                      *((_QWORD *)a3 + 2) = 0;
                      *(_QWORD *)a3 = 0;
                    }
                  }
                }
                else
                {
                  sub_3B7B4(10002203, &unk_59396);
                  v71 = g_opaque_predicate_b;
                  v72 = g_opaque_predicate_a;
                  *((_QWORD *)a3 + 1) = 0;
                  *((_QWORD *)a3 + 2) = 0;
                  *(_QWORD *)a3 = 0;
                  v73 = (v71 - 1) * v71;
                  if ( v72 >= 10 && (v73 & 1) != 0 )
                  {
                    do
                    {
                      sub_3B7B4(10002203, &unk_59396);
                      *((_QWORD *)a3 + 1) = 0;
                      *((_QWORD *)a3 + 2) = 0;
                      *(_QWORD *)a3 = 0;
                      sub_3B7B4(10002203, &unk_59396);
                      v74 = g_opaque_predicate_b;
                      v75 = g_opaque_predicate_a;
                      *((_QWORD *)a3 + 1) = 0;
                      *((_QWORD *)a3 + 2) = 0;
                      *(_QWORD *)a3 = 0;
                      v76 = (v74 - 1) * v74;
                    }
                    while ( v75 >= 10 && (v76 & 1) != 0 );
                  }
                }
              }
              else
              {
                if ( g_opaque_predicate_a >= 0 && v61 < 0 )
                  goto LABEL_152;
                while ( 1 )
                {
                  sub_3B7B4(10002202, &unk_59396);
                  v62 = g_opaque_predicate_b;
                  v63 = g_opaque_predicate_a;
                  *((_QWORD *)a3 + 1) = 0;
                  *((_QWORD *)a3 + 2) = 0;
                  *(_QWORD *)a3 = 0;
                  v64 = (v62 - 1) * v62;
                  if ( v63 < 10 || (v64 & 1) == 0 )
                    break;
LABEL_152:
                  sub_3B7B4(10002202, &unk_59396);
                  *((_QWORD *)a3 + 1) = 0;
                  *((_QWORD *)a3 + 2) = 0;
                  *(_QWORD *)a3 = 0;
                }
              }
              while ( 1 )
              {
                if ( v60 )
                  (*(void (__fastcall **)(__int64))(*(_QWORD *)v60 + 8LL))(v60);
                v66 = g_opaque_predicate_a;
                v68 = (((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1;
                if ( g_opaque_predicate_a < 10
                  || ((((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1) == 0 )
                {
                  break;
                }
                v60 = 0;
              }
            }
            else
            {
              if ( g_opaque_predicate_a >= 10 && (v58 & 1) != 0 )
                goto LABEL_155;
              while ( 1 )
              {
                sub_3B7B4(10002201, v5);
                v65 = g_opaque_predicate_b;
                v66 = g_opaque_predicate_a;
                *((_QWORD *)a3 + 1) = 0;
                *((_QWORD *)a3 + 2) = 0;
                *(_QWORD *)a3 = 0;
                v67 = (v65 - 1) * v65;
                if ( v66 < 0 || (v67 & 0x80000000) == 0 )
                  break;
LABEL_155:
                sub_3B7B4(10002201, v5);
                *((_QWORD *)a3 + 1) = 0;
                *((_QWORD *)a3 + 2) = 0;
                *(_QWORD *)a3 = 0;
              }
              v68 = v67 & 1;
            }
            if ( v66 >= 10 && v68 )
              goto LABEL_163;
            while ( 1 )
            {
              if ( v131 )
              {
                v132 = (unsigned __int64)v131;
                operator delete(v131);
              }
              v14 = g_opaque_predicate_a;
              i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
                break;
LABEL_163:
              if ( v131 )
              {
                v132 = (unsigned __int64)v131;
                operator delete(v131);
              }
            }
          }
          else
          {
LABEL_127:
            sub_3B7B4(10002200, &unk_59396);
            v53 = g_opaque_predicate_b;
            v14 = g_opaque_predicate_a;
            *((_QWORD *)a3 + 1) = 0;
            *((_QWORD *)a3 + 2) = 0;
            *(_QWORD *)a3 = 0;
            for ( i = (v53 - 1) * v53; v14 >= 10 && (i & 1) != 0; i = (v54 - 1) * v54 )
            {
              sub_3B7B4(10002200, &unk_59396);
              *((_QWORD *)a3 + 1) = 0;
              *((_QWORD *)a3 + 2) = 0;
              *(_QWORD *)a3 = 0;
              sub_3B7B4(10002200, &unk_59396);
              v54 = g_opaque_predicate_b;
              v14 = g_opaque_predicate_a;
              *((_QWORD *)a3 + 1) = 0;
              *((_QWORD *)a3 + 2) = 0;
              *(_QWORD *)a3 = 0;
            }
          }
          goto LABEL_383;
        case 10418:
          if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
          {
            while ( 1 )
              ;
          }
          if ( !v5 || !*v5 )
            goto LABEL_378;
          while ( 1 )
          {
            strlen(v5);
            tramp_sign10418_hash();
            v14 = g_opaque_predicate_a;
            v39 = *a3;
            i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
              break;
            strlen(v5);
            tramp_sign10418_hash();
          }
          if ( (v39 & 1) != 0 )
            v40 = *((_QWORD *)a3 + 1);
          else
            v40 = v39 >> 1;
          if ( v40 )
          {
            v41 = 1;
          }
          else
          {
            if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
              goto LABEL_231;
            while ( 1 )
            {
              sub_3C54C();
              v97 = v96;
              sub_3B7B4(10001200, &unk_59396);
              if ( v97 )
                sub_3C614();
              v14 = g_opaque_predicate_a;
              v41 = 0;
              i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
                break;
LABEL_231:
              sub_3C54C();
              v99 = v98;
              sub_3B7B4(10001200, &unk_59396);
              if ( v99 )
                sub_3C614();
            }
          }
          v16 = i & 1;
          while ( v14 >= 10 && (i & 1) != 0 )
            ;
          if ( (v41 & 1) == 0 )
          {
            while ( 1 )
            {
              if ( (*a3 & 1) != 0 )
                operator delete(*((void **)a3 + 2));
              v14 = g_opaque_predicate_a;
              i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
                break;
              if ( (*a3 & 1) != 0 )
                operator delete(*((void **)a3 + 2));
            }
            v16 = i & 1;
          }
          while ( v14 >= 0 && i < 0 )
            ;
          if ( !v41 )
          {
LABEL_378:
            if ( v14 >= 10 && v16 )
              goto LABEL_382;
            while ( 1 )
            {
              v123 = v135;
              v124 = v134;
              v134 = 0u;
              v135 = 0;
              *((_QWORD *)a3 + 2) = v123;
              *(_OWORD *)a3 = v124;
              if ( v14 < 0 || (i & 0x80000000) == 0 )
                break;
LABEL_382:
              v125 = v135;
              v126 = v134;
              v134 = 0u;
              v135 = 0;
              *((_QWORD *)a3 + 2) = v125;
              *(_OWORD *)a3 = v126;
            }
          }
LABEL_383:
          if ( (v14 & 0x80000000) == 0 && i < 0 )
          {
            if ( v13 )
              operator delete(v13);
            while ( 1 )
            {
              if ( v12 )
                sub_3C614();
              if ( (v134 & 1) != 0 )
                operator delete(v135);
LABEL_395:
              if ( (v134 & 1) != 0 )
                operator delete(v135);
              if ( g_opaque_predicate_a < 10 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) == 0 )
                JUMPOUT(0xC000);
              v12 = 0;
            }
          }
          operator delete(v13);
          if ( v12 )
            sub_3C614();
          goto LABEL_395;
        default:
          if ( a2 != 10405 || !v5 )
            goto LABEL_378;
          if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
          {
            while ( 1 )
              ;
          }
          if ( !*v5 )
            goto LABEL_378;
          while ( 1 )
          {
            strlen(v5);
            tramp_sig3_10405_sha256_hex();
            v14 = g_opaque_predicate_a;
            v42 = *a3;
            i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            v43 = i & 1;
            if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
              break;
            strlen(v5);
            tramp_sig3_10405_sha256_hex();
          }
          if ( (v42 & 1) != 0 )
            v44 = *((_QWORD *)a3 + 1);
          else
            v44 = v42 >> 1;
          if ( v44 )
          {
            v45 = 1;
            if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
            {
              while ( 1 )
                ;
            }
          }
          else
          {
            while ( 1 )
            {
              sub_3C54C();
              v49 = v48;
              sub_3B7B4(70024, &unk_59396);
              if ( v49 )
                sub_3C614();
              v14 = g_opaque_predicate_a;
              v45 = 0;
              i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              v43 = i & 1;
              if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
                break;
              sub_3C54C();
              v47 = v46;
              sub_3B7B4(70024, &unk_59396);
              if ( v47 )
                sub_3C614();
            }
          }
          if ( v14 >= 10 && v43 )
          {
            while ( 1 )
              ;
          }
          if ( (v45 & 1) != 0 )
            goto LABEL_253;
          if ( v14 < 0 || (i & 0x80000000) == 0 )
            goto LABEL_247;
          do
          {
            if ( (*a3 & 1) != 0 )
              operator delete(*((void **)a3 + 2));
LABEL_247:
            if ( (*a3 & 1) != 0 )
              operator delete(*((void **)a3 + 2));
            v14 = g_opaque_predicate_a;
            i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
          }
          while ( g_opaque_predicate_a >= 10 && (i & 1) != 0 );
LABEL_253:
          if ( (v14 & 0x80000000) == 0 && i < 0 )
          {
            while ( 1 )
              ;
          }
          if ( v45 )
            goto LABEL_383;
          v16 = i & 1;
          goto LABEL_378;
      }
    }
LABEL_5:
    va_end(va);
    va_start(va, a3);
  }
}

SHA256 wrapper:ida_sha256_hash_wrapper_body.c

  • 文件:output/ida_sha256_hash_wrapper_body.c
  • 行数:45
  • 字节:1093
  • SHA256:6f625e8b7949b3b4663325301ca3a03ee7a72f54f983b468c2028c6eca1725cc

为什么看它:10418 不是普通 Java SHA256,native 里还有 wrapper 和 table manager 路径。

读完得到的结论:Python 里用 kwsg_sha256_digest() 表达这个 native wrapper 行为。

void __fastcall sha256_hash_wrapper_body(_BYTE *a1)
{
  int v1; // w10
  int v2; // w8
  int v3; // w9

  v1 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
  while ( g_opaque_predicate_a >= 0 && v1 < 0 )
    ;
  if ( *a1 )
  {
    if ( g_opaque_predicate_a < 10 || (v1 & 1) == 0 )
      goto LABEL_10;
    do
    {
      tramp_sha256_core();
LABEL_10:
      tramp_sha256_core();
      v2 = g_opaque_predicate_a;
      v3 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
    }
    while ( g_opaque_predicate_a >= 10 && (v3 & 1) != 0 );
  }
  else
  {
    while ( 1 )
    {
      get_zt_table_manager_singleton();
      get_zt_table_manager_singleton();
      tramp_sha256_core();
      v2 = g_opaque_predicate_a;
      v3 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
      if ( g_opaque_predicate_a < 10 || (v3 & 1) == 0 )
        break;
      get_zt_table_manager_singleton();
      get_zt_table_manager_singleton();
      tramp_sha256_core();
    }
  }
  if ( (v2 & 0x80000000) == 0 && v3 < 0 )
  {
    while ( 1 )
      ;
  }
}

10418 hash helper:ida_sign10418_hash_body.c

  • 文件:output/ida_sign10418_hash_body.c
  • 行数:334
  • 字节:9033
  • SHA256:e182199adf6c0549c3c59089e052e8429328c30fd643ed7baceee215d5c8dae3

为什么看它:它是 10418 分支进入 digest、padding、ZT transform 的关键。

读完得到的结论:Python sig3_10418()digest -> PKCS#7 pad -> zt_table_transform -> hex 就来自这条链。

void __usercall sign10418_hash_body(__int64 a2@<X3>, __int64 a3@<X4>, struct timeval *a4@<X8>)
{
  int v5; // w8
  int v6; // w11
  _BOOL4 v7; // w10
  int v8; // w9
  bool v9; // w11
  char v10; // w0
  int v11; // w10
  unsigned __int8 *v12; // x20
  int v13; // w22
  char v14; // w0
  int v15; // w8
  int v16; // w9
  int v17; // w8
  int v18; // w9
  void *v19; // x0
  int v20; // w8
  void *v21; // x20
  int v22; // w9
  char *v23; // x0
  unsigned __int64 tv_usec; // x10
  void *v25; // x8
  struct timeval v26; // q0
  struct timeval v27; // [xsp+8h] [xbp-98h] BYREF
  void *v28; // [xsp+18h] [xbp-88h]
  size_t v29; // [xsp+20h] [xbp-80h]
  void *src; // [xsp+28h] [xbp-78h]
  struct timeval tv; // [xsp+30h] [xbp-70h] BYREF
  void *v32; // [xsp+40h] [xbp-60h]
  __int64 v33; // [xsp+48h] [xbp-58h]
  unsigned __int8 *v34; // [xsp+50h] [xbp-50h]
  __int64 v35; // [xsp+58h] [xbp-48h]

  v35 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
  v5 = g_opaque_predicate_a;
  v6 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
  v7 = g_opaque_predicate_a < 10 || (v6 & 1) == 0;
  v8 = v6 & 1;
  v9 = g_opaque_predicate_a >= 0 && v6 < 0;
  while ( v9 )
    ;
  if ( a2 && a3 )
  {
    if ( v7 )
      goto LABEL_8;
    do
    {
      v33 = 0;
      v34 = 0;
      gettimeofday(&tv, 0);
      tramp_sha256_digest_bytes();
LABEL_8:
      v33 = 0;
      v34 = 0;
      gettimeofday(&tv, 0);
      tramp_sha256_digest_bytes();
      v5 = g_opaque_predicate_a;
      v11 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
    }
    while ( (g_opaque_predicate_a & 0x80000000) == 0 && v11 < 0 );
    if ( (v10 & 1) != 0 )
    {
      v13 = v33;
      v12 = v34;
      while ( 1 )
      {
        bytes_to_lower_hex_string(v12, v13);
        v29 = 0;
        src = 0;
        gettimeofday(&v27, 0);
        sub_1E07C();
        v15 = g_opaque_predicate_a;
        v16 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
        if ( g_opaque_predicate_a < 0 || (v16 & 0x80000000) == 0 )
          break;
        bytes_to_lower_hex_string(v12, v13);
        v29 = 0;
        src = 0;
        gettimeofday(&v27, 0);
        sub_1E07C();
      }
      if ( (v14 & 1) != 0 )
      {
        if ( g_opaque_predicate_a < 10 || (v16 & 1) == 0 )
          goto LABEL_47;
        do
        {
          bytes_to_lower_hex_string((unsigned __int8 *)src, v29);
          if ( (tv.tv_sec & 1) != 0 )
          {
            *(_BYTE *)v32 = 0;
            tv.tv_usec = 0;
            if ( (tv.tv_sec & 1) != 0 )
            {
              operator delete(v32);
              tv.tv_sec = 0;
            }
          }
          else
          {
            LOWORD(tv.tv_sec) = 0;
          }
          v32 = v28;
          tv = v27;
          gettimeofday(&v27, 0);
LABEL_47:
          bytes_to_lower_hex_string((unsigned __int8 *)src, v29);
          if ( (tv.tv_sec & 1) != 0 )
          {
            *(_BYTE *)v32 = 0;
            tv.tv_usec = 0;
            if ( (tv.tv_sec & 1) != 0 )
            {
              operator delete(v32);
              tv.tv_sec = 0;
            }
          }
          else
          {
            LOWORD(tv.tv_sec) = 0;
          }
          v32 = v28;
          tv = v27;
          gettimeofday(&v27, 0);
          v17 = g_opaque_predicate_a;
          v18 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
        }
        while ( g_opaque_predicate_a >= 10 && (v18 & 1) != 0 );
        if ( v12 )
        {
          while ( 1 )
          {
            free(v12);
            v17 = g_opaque_predicate_a;
            v18 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            if ( g_opaque_predicate_a < 10 || (v18 & 1) == 0 )
              break;
            free(v12);
          }
        }
        if ( (v17 & 0x80000000) == 0 && v18 < 0 )
          goto LABEL_70;
        while ( 1 )
        {
          v27.tv_usec = 0;
          v28 = 0;
          v27.tv_sec = 0;
          std::string::resize(&v27, v29, 0);
          v19 = (v27.tv_sec & 1) != 0 ? v28 : (char *)&v27.tv_sec + 1;
          memcpy(v19, src, v29);
          v20 = g_opaque_predicate_a;
          v21 = src;
          v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
          if ( g_opaque_predicate_a < 0 || (v22 & 0x80000000) == 0 )
            break;
LABEL_70:
          v27.tv_usec = 0;
          v28 = 0;
          v27.tv_sec = 0;
          std::string::resize(&v27, v29, 0);
          if ( (v27.tv_sec & 1) != 0 )
            v23 = (char *)v28;
          else
            v23 = (char *)&v27.tv_sec + 1;
          memcpy(v23, src, v29);
        }
        if ( (v27.tv_sec & 1) != 0 )
          tv_usec = v27.tv_usec;
        else
          tv_usec = (unsigned __int64)LOBYTE(v27.tv_sec) >> 1;
        if ( tv_usec )
        {
          if ( g_opaque_predicate_a >= 10 && (v22 & 1) != 0 )
          {
            while ( 1 )
              ;
          }
          if ( src )
          {
            while ( 1 )
            {
              free(v21);
              v20 = g_opaque_predicate_a;
              v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 0 || (v22 & 0x80000000) == 0 )
                break;
              free(v21);
            }
          }
          if ( v20 >= 10 && (v22 & 1) != 0 )
          {
            while ( 1 )
            {
              a4->tv_sec = 0;
              a4->tv_usec = 0;
              a4[1].tv_sec = 0;
              v27.tv_usec = 0;
              v28 = 0;
              v27.tv_sec = 0;
            }
          }
          v25 = v28;
          v26 = v27;
          v27.tv_sec = 0;
          v27.tv_usec = 0;
          v28 = 0;
          a4[1].tv_sec = (__time_t)v25;
          *a4 = v26;
        }
        else
        {
          if ( src )
          {
            if ( g_opaque_predicate_a >= 10 && (v22 & 1) != 0 )
              goto LABEL_96;
            while ( 1 )
            {
              free(v21);
              v20 = g_opaque_predicate_a;
              v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
              if ( g_opaque_predicate_a < 10 || (v22 & 1) == 0 )
                break;
LABEL_96:
              free(v21);
            }
          }
          if ( (v20 & 0x80000000) == 0 && v22 < 0 )
            goto LABEL_101;
          while ( 1 )
          {
            a4->tv_sec = 0;
            a4->tv_usec = 0;
            a4[1].tv_sec = 0;
            if ( v20 < 10 || (v22 & 1) == 0 )
              break;
LABEL_101:
            a4->tv_sec = 0;
            a4->tv_usec = 0;
            a4[1].tv_sec = 0;
          }
        }
        while ( 1 )
        {
          if ( (v27.tv_sec & 1) != 0 )
            operator delete(v28);
          if ( g_opaque_predicate_a < 10 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) == 0 )
            break;
          if ( (v27.tv_sec & 1) != 0 )
            operator delete(v28);
        }
      }
      else
      {
        if ( g_opaque_predicate_a >= 10 && (v16 & 1) != 0 )
        {
          while ( 1 )
            ;
        }
        if ( v12 )
        {
          while ( 1 )
          {
            free(v12);
            v15 = g_opaque_predicate_a;
            v16 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
            if ( g_opaque_predicate_a < 0 || (v16 & 0x80000000) == 0 )
              break;
            free(v12);
          }
        }
        if ( v15 >= 10 && (v16 & 1) != 0 )
          goto LABEL_33;
        while ( 1 )
        {
          a4->tv_sec = 0;
          a4->tv_usec = 0;
          a4[1].tv_sec = 0;
          if ( v15 < 0 || (v16 & 0x80000000) == 0 )
            break;
LABEL_33:
          a4->tv_sec = 0;
          a4->tv_usec = 0;
          a4[1].tv_sec = 0;
        }
        if ( v15 >= 10 && (v16 & 1) != 0 )
          goto LABEL_35;
      }
      while ( 1 )
      {
        if ( (tv.tv_sec & 1) != 0 )
          operator delete(v32);
        v5 = g_opaque_predicate_a;
        v11 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
        v8 = v11 & 1;
        if ( g_opaque_predicate_a < 10 || (v11 & 1) == 0 )
          break;
LABEL_35:
        if ( (tv.tv_sec & 1) != 0 )
          operator delete(v32);
      }
LABEL_41:
      while ( (v5 & 0x80000000) == 0 )
      {
LABEL_42:
        if ( (v11 & 0x80000000) == 0 )
          break;
      }
    }
    else
    {
      v8 = v11 & 1;
      a4->tv_sec = 0;
      a4->tv_usec = 0;
      a4[1].tv_sec = 0;
      if ( v5 < 10 || (v11 & 1) == 0 )
        goto LABEL_41;
      v8 = 1;
      if ( (v5 & 0x80000000) == 0 )
        goto LABEL_42;
    }
  }
  else
  {
    a4->tv_sec = 0;
    a4->tv_usec = 0;
    a4[1].tv_sec = 0;
  }
  if ( v5 >= 10 && v8 )
  {
    while ( 1 )
      ;
  }
}

IDA 到 Python 的映射理由

从 IDA 回到 Python,不是逐行翻译伪代码。混淆函数里大量循环是 opaque predicate,真正要保留的是语义:

IDA 现象 语义 Python 落点
case 10417 + atomic counter 10417 有计数器/时间形态 sig3_10417(data, table, now, counter)
case 10418 + tramp_sign10418_hash() 10418 走 native hash helper sig3_10418(data, table)
a2 != 10405 default 判断 老模式 10405 sig3_10405(data, table, now)
table manager / transform helper ZT 表参与输出变换 KwaiZtTable / zt_table_transform()

0x05 ZT、sig、sig3、DFP、Azeroth:算法层完整实现

到这里 Java 和 IDA 只告诉我们“应该怎么走”。真正能重复跑、能单测、能接 REST 的,是 Python 算法层。下面完整内联 kws_recovered/algorithms.py

算法层完整实现:kws_recovered/algorithms.py

  • 文件:kws_recovered/algorithms.py
  • 行数:871
  • 字节:31401
  • SHA256:800ad3eae0c395bb0664f3cdacd6a35e2205b0c61b50dbe42e5d010869304608

为什么看它:这里把 Java/IDA 证据落成可执行算法:kwai_sigkwai_token_sigsig3_10405/10417/10418、PNG/动态容器 ZT 表、DFP、Azeroth HMAC。

读完得到的结论:它是所有 REST wrapper 的底座,也是测试里最先固定的部分。

from __future__ import annotations

import base64
import binascii
import hashlib
import hmac
import json
import os
import random
import struct
import time
import urllib.parse
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Mapping, MutableMapping, Optional


APPKEY_MAIN = "7e46b28a-8c93-4940-8238-4c60e64e3c81"
APPKEY_AZEROTH = "010a11c6-f2cb-4016-887d-0d958aef1534"
KWE_N = "KWE_N"
DFP_SDK_VERSION = "8.9.1antman.40.05d13a7f"
DFP_SDK_PLATFORM = "AND"
GET_CLOCK_SALT = b"382700b563f4"
GET_MAGIC_VALUE = "46a8qpMw6643TDiV"

KWSG_STATIC_SHA256_KEY00 = bytes.fromhex(
    "58525c4478457c47007000675d0e435740556c620300710464654554545362"
    "676e006479537f52466e7763635b036e5e05745a7c427f616c40045f54414f6072"
)
KWSG_STATIC_SHA256_KEY01 = bytes.fromhex(
    "3238362e122f162d6a1a6a0d3764293d2a3f0608696a1b6e0e0f2f3e3e39080d"
    "046a0e133915382c041d0909316904346f1e301628150b062a6e353e2b250a18"
)


def kwsg_sha256_digest(data: bytes, table: KwaiZtTable | None = None, *, inner: bool = False) -> bytes:
    if inner:
        key00 = KWSG_STATIC_SHA256_KEY00
        key01 = KWSG_STATIC_SHA256_KEY01
    elif table is not None and table.key00 and table.key01:
        key00 = table.key00
        key01 = table.key01
    else:
        return hashlib.sha256(data).digest()
    return hashlib.sha256(key01 + hashlib.sha256(key00 + data).digest()).digest()

LITE_DFP_KEYS = (
    "k5",
    "k14",
    "k22",
    "k27",
    "k29",
    "k31",
    "k34",
    "k35",
    "k36",
    "k39",
    "k40",
    "k46",
    "k57",
    "k64",
    "k66",
    "k68",
    "k83",
    "k86",
    "k93",
    "k97",
    "k101",
    "k102",
    "k105",
    "k106",
    "k107",
    "k108",
    "k109",
    "k110",
    "k111",
    "k112",
    "k113",
)


@dataclass(frozen=True)
class KwaiZtTable:
    appkey: str
    platform: str
    version: int
    scopes: tuple[str, ...]
    x5: str
    raw: Mapping[str, object]
    table_a: bytes = b""
    table_b: bytes = b""
    table_c: bytes = b""
    key00: bytes = b""
    key01: bytes = b""

    @property
    def selector_byte(self) -> int:
        # Native takes byte 8 from x1/appkey; for UUID appkeys this is '-'.
        return ord(self.appkey[8]) if len(self.appkey) > 8 else 2

    @property
    def header_word(self) -> int:
        # load_zt_table_for_appkey_body stores sdk.x3 at table+0x30; zt_wrap reads
        # that word via an internal pointer and XORs it with 0xCDEF.
        return self.version & 0xFFFF


def crc32_ieee(data: bytes) -> int:
    return binascii.crc32(data) & 0xFFFFFFFF


def _u16le(value: int) -> bytes:
    return struct.pack("<H", value & 0xFFFF)


def _u32le(value: int) -> bytes:
    return struct.pack("<I", value & 0xFFFFFFFF)


def get_magic() -> str:
    return GET_MAGIC_VALUE


def get_clock(data: bytes | str) -> str:
    if isinstance(data, str):
        data = data.encode("utf-8")
    return hashlib.md5(data + GET_CLOCK_SALT).hexdigest()


def java_value(value: object | None) -> str:
    return "" if value is None else str(value)


def kwai_canonical_params(*maps: Mapping[str, object | None]) -> bytes:
    parts: list[str] = []
    for params in maps:
        for key, value in params.items():
            parts.append(f"{key}={java_value(value)}")
    return "".join(sorted(parts)).encode("utf-8")


def kwai_sig(*maps: Mapping[str, object | None]) -> str:
    return get_clock(kwai_canonical_params(*maps))


def kwai_token_sig(sig: str, client_salt: str) -> str:
    return hashlib.sha256((sig + client_salt).encode("utf-8")).hexdigest()


def sig3_10405(
    data: bytes | str,
    appkey: str = APPKEY_MAIN,
    now: int | None = None,
    table: KwaiZtTable | None = None,
) -> str:
    if isinstance(data, str):
        data = data.encode("utf-8")
    if now is None:
        now = int(time.time())
    sha_hex = hashlib.sha256(data).hexdigest()
    prefix = str((now ^ 0xDDCC0DEF) & 0xFFFFFFFF)[:9]
    selector = table.selector_byte if table else (ord(appkey[8]) if len(appkey) > 8 else 0)
    middle = "9" + f"{(selector ^ 0xCD) & 0xFF:02x}"
    return prefix + middle + sha_hex[:30]


def _sig3_feature_flags(feature_bits: int) -> int:
    out = 0x0D00
    if (feature_bits >> 61) & 1:
        out |= 0x01
    if (feature_bits >> 57) & 1:
        out |= 0x02
    if (feature_bits >> 60) & 1:
        out |= 0x04
    if (feature_bits >> 53) & 1:
        out |= 0x10
    if (feature_bits >> 54) & 1:
        out |= 0x20
    if (feature_bits >> 44) & 1:
        out |= 0x40
    return out


def _sig3_10417_payload(
    data: bytes,
    selector: int,
    runtime_value: int,
    counter: int,
    now_seconds: int,
    feature_bits: int,
) -> bytes:
    payload = bytearray(24)
    payload[0:2] = b"AQ"
    payload[2:4] = _u16le(selector)
    payload[4:8] = _u32le(runtime_value)
    payload[8:12] = _u32le(counter)
    payload[12:16] = _u32le(crc32_ieee(data))
    payload[16:20] = _u32le(now_seconds)
    payload[20:24] = _u32le(_sig3_feature_flags(feature_bits))
    checksum = sum(payload[:23])
    payload[23] = (checksum if checksum <= 0xFF else -checksum) & 0xFF
    for index in range(23):
        payload[index] ^= payload[23] ^ index
    return bytes(payload)


def zt_wrap_payload_hex(payload_hex: str, table: KwaiZtTable) -> str:
    raw_payload = bytes.fromhex(payload_hex)
    key = table.x5.encode("utf-8")
    if not key:
        return payload_hex
    header = bytearray()
    header += b"ZT"
    header += _u16le(table.header_word ^ 0xCDEF)
    header += _u32le(crc32_ieee(table.platform.encode("utf-8")))
    encrypted = bytes(byte ^ key[index & 0x0F] for index, byte in enumerate(raw_payload))
    return header.hex() + encrypted.hex()


def sig3_10417(
    data: bytes | str,
    table: KwaiZtTable | None = None,
    now: int | None = None,
    counter: int = 1,
    runtime_value: int = 0,
    feature_bits: int = 0,
    wrap: bool = True,
) -> str:
    if isinstance(data, str):
        data = data.encode("utf-8")
    if now is None:
        now = int(time.time())
    selector = table.selector_byte if table else 2
    payload_hex = _sig3_10417_payload(
        data=data,
        selector=selector,
        runtime_value=runtime_value,
        counter=counter,
        now_seconds=now,
        feature_bits=feature_bits,
    ).hex()
    if wrap and table:
        return zt_wrap_payload_hex(payload_hex, table)
    return payload_hex


def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
    pad_len = block_size - (len(data) % block_size)
    return data + bytes([pad_len]) * pad_len


def _aes_ecb_encrypt(key: bytes, data: bytes) -> bytes:
    try:
        from Crypto.Cipher import AES

        return AES.new(key, AES.MODE_ECB).encrypt(data)
    except Exception:
        from cryptography.hazmat.backends import default_backend
        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

        encryptor = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
        return encryptor.update(data) + encryptor.finalize()


_KWAi_PAYLOAD_XOR = bytes(
    [0x1D, 0x1E, 0x17, 0x0B, 0x1D, 0x15, 0x1B, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x0C]
)
_KWAi_NAME_XOR = bytes(
    [0x1D, 0x17, 0x0B, 0x1D, 0x15, 0x1A, 0x1B, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x0C]
)
_ZT_PERMUTE = (0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11)


def _u32le_from(data: bytes, offset: int) -> int:
    return struct.unpack_from("<I", data, offset)[0]


def native_obfuscated_string(encoded: bytes) -> str:
    """Decode the one-byte-length string format used by native sub_3C54C."""
    if not encoded:
        return ""
    length = encoded[0]
    seed = b"Vuz4fCHxn1CO"
    state_a = 324508639
    state_b = 610839776
    state_c = (-38177487) & 0xFFFFFFFF
    for byte in seed[4:8]:
        state_a = ((state_a << 8) | byte) & 0xFFFFFFFF
        state_b = ((state_b << 8) | byte) & 0xFFFFFFFF
        state_c = ((state_c << 8) | byte) & 0xFFFFFFFF
    out = bytearray()
    for byte in encoded[1 : 1 + length]:
        bit_c = state_c & 1
        bit_b = state_b & 1
        stream_byte = 0
        mixed = 0
        for _ in range(8):
            if state_a & 1:
                state_a = (0x40000031 ^ state_a | 0x80000000) & 0xFFFFFFFF
                if state_b & 1:
                    state_b = (0x20000010 ^ state_b | 0xC0000000) & 0xFFFFFFFF
                    bit_b = 1
                else:
                    state_b = (0x3FFFFFFF & (state_b >> 1)) & 0xFFFFFFFF
                    bit_b = 0
            else:
                state_a = (0x7FFFFFFF & (state_a >> 1)) & 0xFFFFFFFF
                if state_c & 1:
                    state_c = (0x08000001 ^ state_c | 0xF0000000) & 0xFFFFFFFF
                    bit_c = 1
                else:
                    state_c = (0x0FFFFFFF & (state_c >> 1)) & 0xFFFFFFFF
                    bit_c = 0
            mixed = (bit_c ^ bit_b) | (2 * stream_byte)
            stream_byte = mixed & 0xFF
        out.append(((mixed + 3) & 0xFF) ^ byte)
    return out.decode("latin1")


def decode_kwai_container(blob: bytes) -> dict[str, bytes]:
    """Decode the native `kwai` resource container used for zt pass tables.

    The CDN/asset file keeps a small plaintext directory, then a zlib stream whose
    first two bytes are deliberately clobbered. Native restores them to 0x78da
    before inflating and treats directory offsets as if the directory header were
    still present, so extracted offsets are adjusted by the first data offset.
    Directory names and payload bytes use adjacent but different 16-byte XOR keys.
    """
    if not blob.startswith(b"kwai"):
        raise ValueError("not a kwai zt resource container")
    compressed_size = int.from_bytes(blob[12:16], "big")
    entry_count = blob[20]
    cursor = 21
    entries: list[tuple[str, int, int]] = []
    for _ in range(entry_count):
        name_size = int.from_bytes(blob[cursor : cursor + 4], "big")
        cursor += 4
        encoded_name = blob[cursor : cursor + name_size]
        cursor += name_size
        offset = int.from_bytes(blob[cursor : cursor + 4], "big")
        size = int.from_bytes(blob[cursor + 4 : cursor + 8], "big")
        cursor += 8
        name = bytes(byte ^ _KWAi_NAME_XOR[index & 0x0F] for index, byte in enumerate(encoded_name)).decode(
            "latin1"
        )
        entries.append((name, offset, size))
    first_offset = min(offset for _, offset, _ in entries)
    compressed = bytearray(blob[first_offset : first_offset + compressed_size])
    if len(compressed) < 2:
        raise ValueError("empty kwai zt compressed payload")
    compressed[0:2] = b"\x78\xda"
    inflated = zlib.decompress(bytes(compressed))
    decoded: dict[str, bytes] = {}
    for name, offset, size in entries:
        data = inflated[offset - first_offset : offset - first_offset + size]
        decoded[name] = bytes(byte ^ _KWAi_PAYLOAD_XOR[index & 0x0F] for index, byte in enumerate(data))
    return decoded


def load_kwai_dynamic_sections(path: str | os.PathLike[str]) -> dict[str, bytes]:
    return decode_kwai_container(Path(path).read_bytes())


def _with_dynamic_sections(table: KwaiZtTable, sections: Mapping[str, bytes]) -> KwaiZtTable:
    return KwaiZtTable(
        appkey=table.appkey,
        platform=table.platform,
        version=table.version,
        scopes=table.scopes,
        x5=table.x5,
        raw=table.raw,
        table_a=sections.get("TBoxes_40960", sections.get("TKsnm|_40960", table.table_a)),
        table_b=sections.get("TyiBoxes_147456", sections.get("TpuTgwes_147456", table.table_b)),
        table_c=sections.get("TyiTables_4096", sections.get("TpuBimles_4096", table.table_c)),
        key00=sections.get("sha256key00_64", sections.get("sa}$=9key00_64", table.key00)),
        key01=sections.get("sha256key01_64", sections.get("sa}$=9key01_64", table.key01)),
    )


def _zt_permute_block(block: bytearray) -> None:
    original = bytes(block)
    for index, source_index in enumerate(_ZT_PERMUTE):
        block[index] = original[source_index]


def _zt_table_transform_block(block: bytes, table: KwaiZtTable) -> bytes:
    if len(block) != 16:
        raise ValueError("zt table transform operates on 16-byte blocks")
    if len(table.table_a) < 40960 or len(table.table_b) < 147456:
        raise ValueError("kwai zt dynamic pass tables are missing")
    state = bytearray(block)
    table_a = table.table_a
    table_b = table.table_b
    for round_index in range(9):
        _zt_permute_block(state)
        round_base = round_index << 14
        for column in range(4):
            byte_pos = column << 2
            value = 0
            for sub_index in range(4):
                state_index = byte_pos | sub_index
                table_offset = round_base + (state_index << 10) + (state[state_index] << 2)
                value ^= _u32le_from(table_b, table_offset)
            state[byte_pos] = (value >> 24) & 0xFF
            state[byte_pos | 1] = (value >> 16) & 0xFF
            state[byte_pos | 2] = (value >> 8) & 0xFF
            state[byte_pos | 3] = value & 0xFF
    _zt_permute_block(state)
    for index, value in enumerate(state):
        state[index] = table_a[36864 + (index << 8) + value]
    return bytes(state)


def _zt_dynamic_op(table_c: bytes, left: int, right: int) -> int:
    return table_c[((left & 0xFF) << 4) + (right & 0xFF)]


def _zt_dynamic_combine_words(first: int, second: int, third: int, fourth: int, table_c: bytes) -> bytes:
    first &= 0xFFFFFFFF
    second &= 0xFFFFFFFF
    third &= 0xFFFFFFFF
    fourth &= 0xFFFFFFFF

    mid_12 = _zt_dynamic_op(table_c, (first >> 12) & 0xF, (second >> 12) & 0xF)
    mid_08 = _zt_dynamic_op(table_c, (first >> 8) & 0xF, (second >> 8) & 0xF)
    mid_20 = _zt_dynamic_op(table_c, (first >> 20) & 0xF, (second >> 20) & 0xF)
    mid_04 = _zt_dynamic_op(table_c, (first >> 4) & 0xF, (second >> 4) & 0xF)
    mid_16 = _zt_dynamic_op(table_c, (first >> 16) & 0xF, (second >> 16) & 0xF)
    low_04 = _zt_dynamic_op(table_c, (third >> 4) & 0xF, (fourth >> 4) & 0xF)
    mid_00 = _zt_dynamic_op(table_c, first & 0xF, second & 0xF)
    mid_28 = _zt_dynamic_op(table_c, (first >> 28) & 0xF, (second >> 28) & 0xF)
    low_00 = _zt_dynamic_op(table_c, third & 0xF, fourth & 0xF)
    mid_24 = _zt_dynamic_op(table_c, (first >> 24) & 0xF, (second >> 24) & 0xF)
    low_12 = _zt_dynamic_op(table_c, (third >> 12) & 0xF, (fourth >> 12) & 0xF)
    low_20 = _zt_dynamic_op(table_c, (third >> 20) & 0xF, (fourth >> 20) & 0xF)
    low_08 = _zt_dynamic_op(table_c, (third >> 8) & 0xF, (fourth >> 8) & 0xF)
    low_16 = _zt_dynamic_op(table_c, (third >> 16) & 0xF, (fourth >> 16) & 0xF)
    low_24 = _zt_dynamic_op(table_c, (third >> 24) & 0xF, (fourth >> 24) & 0xF)
    low_28 = _zt_dynamic_op(table_c, (third >> 28) & 0xF, (fourth >> 28) & 0xF)

    out0_low = _zt_dynamic_op(table_c, mid_24, low_24)
    out0_high = _zt_dynamic_op(table_c, mid_28, low_28)
    out1_low = _zt_dynamic_op(table_c, mid_16, low_16)
    out1_high = _zt_dynamic_op(table_c, mid_20, low_20)
    out2_low = _zt_dynamic_op(table_c, mid_08, low_08)
    out2_high = _zt_dynamic_op(table_c, mid_12, low_12)
    out3_low = _zt_dynamic_op(table_c, mid_00, low_00)
    out3_high = _zt_dynamic_op(table_c, mid_04, low_04)
    return bytes(
        (
            (out0_low | (out0_high << 4)) & 0xFF,
            (out1_low | (out1_high << 4)) & 0xFF,
            (out2_low | (out2_high << 4)) & 0xFF,
            (out3_low | (out3_high << 4)) & 0xFF,
        )
    )


def _zt_table_transform_dynamic_block(block: bytes, table: KwaiZtTable) -> bytes:
    if len(block) != 16:
        raise ValueError("zt table transform operates on 16-byte blocks")
    if len(table.table_a) < 40960 or len(table.table_b) < 147456 or len(table.table_c) < 4096:
        raise ValueError("kwai zt dynamic pass tables are missing")
    state = bytearray(block)
    table_a = table.table_a
    table_b = table.table_b
    table_c = table.table_c
    for round_index in range(9):
        _zt_permute_block(state)
        round_base = round_index << 14
        for column in range(4):
            byte_pos = column << 2
            words = []
            for sub_index in range(4):
                state_index = byte_pos | sub_index
                table_offset = round_base + (state_index << 10) + (state[state_index] << 2)
                words.append(_u32le_from(table_b, table_offset))
            state[byte_pos : byte_pos + 4] = _zt_dynamic_combine_words(*words, table_c=table_c)
    _zt_permute_block(state)
    for index, value in enumerate(state):
        state[index] = table_a[36864 + (index << 8) + value]
    return bytes(state)


def zt_table_transform(data: bytes, table: KwaiZtTable, *, inner: bool = False) -> bytes:
    if len(data) % 16:
        raise ValueError("zt table transform input must be 16-byte aligned")
    out = bytearray()
    # Native 10418 has two call sites (normal atlasSign and MXSec/DFP inner),
    # but both reach the same nibble-XOR combine table at byte_59080.  The inner
    # path only picks a different manager slot for the same TBoxes/TyiBoxes data;
    # TyiTables_4096 belongs to a different transform and must not be used here.
    block_transform = _zt_table_transform_block
    for offset in range(0, len(data), 16):
        out += block_transform(data[offset : offset + 16], table)
    return bytes(out)


def sig3_10418(
    data: bytes | str,
    table: KwaiZtTable,
    *,
    inner: bool = False,
) -> str:
    if isinstance(data, str):
        data = data.encode("utf-8")
    # Command 10418 computes the native SHA wrapper digest, PKCS#7-pads the
    # raw 32-byte digest, then applies the zt pass-table transform.  The first
    # bytes_to_lower_hex_string() call in libkwsgmain.so:0x1E4E8 is a temporary
    # std::string side path; sub_1E07C still receives the raw digest pointer and
    # length.  MXSec/DFP passes inner=true, selecting the embedded static pads.
    digest = kwsg_sha256_digest(data, table, inner=inner)
    return zt_table_transform(_pkcs7_pad(digest), table, inner=inner).hex()


def _png_paeth(a: int, b: int, c: int) -> int:
    p = a + b - c
    pa = abs(p - a)
    pb = abs(p - b)
    pc = abs(p - c)
    if pa <= pb and pa <= pc:
        return a
    if pb <= pc:
        return b
    return c


def _png_unfilter(raw: bytes, width: int, height: int, channels: int) -> bytes:
    stride = width * channels
    bpp = channels
    out = bytearray()
    cursor = 0
    prev = bytearray(stride)
    for _ in range(height):
        filter_type = raw[cursor]
        cursor += 1
        row = bytearray(raw[cursor : cursor + stride])
        cursor += stride
        for i, value in enumerate(row):
            left = row[i - bpp] if i >= bpp else 0
            up = prev[i]
            up_left = prev[i - bpp] if i >= bpp else 0
            if filter_type == 1:
                row[i] = (value + left) & 0xFF
            elif filter_type == 2:
                row[i] = (value + up) & 0xFF
            elif filter_type == 3:
                row[i] = (value + ((left + up) >> 1)) & 0xFF
            elif filter_type == 4:
                row[i] = (value + _png_paeth(left, up, up_left)) & 0xFF
            elif filter_type != 0:
                raise ValueError(f"unsupported PNG filter {filter_type}")
        out.extend(row)
        prev = row
    return bytes(out)


def _extract_json_object(data: bytes) -> dict[str, object]:
    start = data.find(b"{")
    if start < 0:
        raise ValueError("hidden PNG payload does not contain JSON")
    depth = 0
    in_string = False
    escape = False
    for pos in range(start, len(data)):
        byte = data[pos]
        if in_string:
            if escape:
                escape = False
            elif byte == 0x5C:
                escape = True
            elif byte == 0x22:
                in_string = False
            continue
        if byte == 0x22:
            in_string = True
        elif byte == 0x7B:
            depth += 1
        elif byte == 0x7D:
            depth -= 1
            if depth == 0:
                return json.loads(data[start : pos + 1].decode("utf-8"))
    raise ValueError("unterminated hidden PNG JSON")


def decode_zt_png(path: str | os.PathLike[str]) -> KwaiZtTable:
    blob = Path(path).read_bytes()
    if not blob.startswith(b"\x89PNG\r\n\x1a\n"):
        raise ValueError(f"not a PNG file: {path}")
    pos = 8
    width = height = bit_depth = color_type = None
    idat = bytearray()
    while pos < len(blob):
        length = struct.unpack(">I", blob[pos : pos + 4])[0]
        chunk_type = blob[pos + 4 : pos + 8]
        chunk_data = blob[pos + 8 : pos + 8 + length]
        pos += 12 + length
        if chunk_type == b"IHDR":
            width, height, bit_depth, color_type = struct.unpack(">IIBB", chunk_data[:10])
        elif chunk_type == b"IDAT":
            idat.extend(chunk_data)
        elif chunk_type == b"IEND":
            break
    if width is None or height is None or bit_depth != 8:
        raise ValueError("unsupported PNG metadata")
    channels_by_type = {0: 1, 2: 3, 4: 2, 6: 4}
    channels = channels_by_type.get(color_type)
    if channels not in (3, 4):
        raise ValueError(f"unsupported zt PNG color type {color_type}")
    pixels = _png_unfilter(zlib.decompress(bytes(idat)), width, height, channels)
    triples: list[tuple[int, int, int]] = []
    for offset in range(0, len(pixels), channels):
        triples.append((pixels[offset], pixels[offset + 1], pixels[offset + 2]))
    if not triples:
        raise ValueError("empty zt PNG")
    first_r, first_g, first_b = triples[0]
    selector = ((first_r & 1) << 2) | ((first_g & 1) << 1) | (first_b & 1)
    orders = {
        0: (0, 1, 2),
        1: (0, 2, 1),
        2: (1, 0, 2),
        3: (1, 2, 0),
        4: (1, 0, 2),  # bundled tables use selector 4 => G,R,B
        5: (2, 0, 1),
        6: (2, 1, 0),
        7: (0, 1, 2),
    }
    order = orders[selector]
    bits: list[int] = []
    for rgb in triples[1:]:
        for channel_index in order:
            bits.append(rgb[channel_index] & 1)
    hidden = bytearray()
    for offset in range(0, len(bits) - 7, 8):
        value = 0
        for bit in bits[offset : offset + 8]:
            value = (value << 1) | bit
        hidden.append(value)
    parsed = _extract_json_object(bytes(hidden))
    sdk = parsed.get("sdk") if isinstance(parsed.get("sdk"), dict) else parsed
    if not isinstance(sdk, dict):
        raise ValueError("zt JSON does not contain sdk object")
    scopes = sdk.get("x4") or []
    if isinstance(scopes, str):
        scopes_tuple = (scopes,)
    else:
        scopes_tuple = tuple(str(item) for item in scopes)
    return KwaiZtTable(
        appkey=str(sdk.get("x1", "")),
        platform=str(sdk.get("x2", "")),
        version=int(str(sdk.get("x3", "0")) or "0"),
        scopes=scopes_tuple,
        x5=str(sdk.get("x5", "")),
        raw=parsed,
    )


def find_zt_table(appkey: str, assets_root: str | os.PathLike[str] = "resources/assets") -> KwaiZtTable:
    root = Path(assets_root)
    table = decode_zt_png(root / "saio_res" / f"zt_{appkey}.png")
    dynamic_path = root / "video_yh_loading_icon.png"
    if dynamic_path.exists():
        try:
            table = _with_dynamic_sections(table, load_kwai_dynamic_sections(dynamic_path))
        except Exception:
            pass
    return table


def varint(value: int) -> bytes:
    out = bytearray()
    while value >= 0x80:
        out.append((value & 0x7F) | 0x80)
        value >>= 7
    out.append(value)
    return bytes(out)


def proto_string(field_number: int, value: object | None) -> bytes:
    if value is None:
        return b""
    data = str(value).encode("utf-8")
    if not data:
        return b""
    return varint((field_number << 3) | 2) + varint(len(data)) + data


def encode_dfp_proto(fields: Mapping[str, object | None]) -> bytes:
    encoded = bytearray()
    for key in sorted(fields, key=lambda item: int(item[1:]) if item.startswith("k") and item[1:].isdigit() else 10**9):
        if key.startswith("k") and key[1:].isdigit():
            encoded += proto_string(int(key[1:]), fields[key])
    return bytes(encoded)


def build_lite_dfp_fields(
    values: Mapping[str, object | None] | None = None,
    android_release: str = KWE_N,
    sdk_version: str = DFP_SDK_VERSION,
    sdk_platform: str = DFP_SDK_PLATFORM,
) -> dict[str, str]:
    values = values or {}
    fields: dict[str, str] = {key: KWE_N for key in LITE_DFP_KEYS}
    fields.update({key: java_value(value) or KWE_N for key, value in values.items() if key in fields})
    fields["k14"] = sdk_platform
    fields["k35"] = java_value(values.get("k35", android_release)) or KWE_N
    fields["k36"] = java_value(values.get("k36", sdk_version)) or KWE_N
    crc = 0
    for key in LITE_DFP_KEYS:
        update_value = sdk_platform if key == "k14" else fields.get(key, "")
        if update_value:
            crc = binascii.crc32(update_value.encode("utf-8"), crc)
    fields["k14"] = f"{sdk_platform}:{crc & 0xFFFFFFFF}"
    return fields


def build_lite_dfp(
    values: Mapping[str, object | None] | None = None,
    *,
    encrypt: bool = False,
    table: KwaiZtTable | None = None,
    android_release: str = KWE_N,
) -> str:
    fields = build_lite_dfp_fields(values, android_release=android_release)
    raw_proto = encode_dfp_proto(fields)
    payload = zt_atlas_encrypt_like(raw_proto, table) if encrypt and table else raw_proto
    return urllib.parse.quote(base64.b64encode(payload).decode("ascii"), safe="")


def _time_prefix(now: int | None = None) -> bytes:
    if now is None:
        now = int(time.time())
    return str((now ^ 0xDDCC0DEF) & 0xFFFFFFFF)[:9].encode("ascii")


def _zt_appkey_header_fields(table: KwaiZtTable) -> tuple[int, bytes, bytes, int]:
    appkey_bytes = table.appkey.encode("utf-8")
    if len(appkey_bytes) < 17:
        selector = table.selector_byte
        return selector, bytes((selector, selector)), _u32le(crc32_ieee(appkey_bytes)), selector
    return appkey_bytes[8], appkey_bytes[9:11], appkey_bytes[12:16], appkey_bytes[16]


def zt_atlas_encrypt_like(
    data: bytes,
    table: KwaiZtTable,
    *,
    now: int | None = None,
    enable_header: bool = True,
) -> bytes:
    # Recovered native 10400/10406 family path: mode=1 pack, then zt_wrap
    # case 1. This returns binary; Java Base64-encodes it for DFP.
    key = table.x5.encode("utf-8")
    if not key:
        return data
    table0, table_word1, table_dword4, table8 = _zt_appkey_header_fields(table)
    if enable_header:
        inner = bytearray(32)
        inner[0:4] = _u32le(0xDEADC0DE)
        inner[4:6] = _u16le(32)
        inner[6:15] = _time_prefix(now)
        inner[15] = table0
        inner[16:18] = table_word1
        inner[18] = 1
        inner[19:23] = table_dword4
        inner[23] = table8
        inner[24:28] = _u32le(crc32_ieee(data))
        inner[28:32] = _u32le(len(data))
        packed = bytes(inner) + data
    else:
        inner = bytearray(17)
        inner[0] = table0
        inner[1:3] = table_word1
        inner[3] = 0
        inner[4:8] = table_dword4
        inner[8] = table8
        inner[9:13] = _u32le(crc32_ieee(data))
        inner[13:17] = _u32le(len(data))
        packed = bytes(inner) + data
    header = b"ZT" + _u16le(table.header_word ^ 0xCDEF) + _u32le(crc32_ieee(table.platform.encode("utf-8")))
    encrypted = bytes(byte ^ key[index & 0x0F] for index, byte in enumerate(packed))
    return header + encrypted


def rc4_ksa_prga(key: bytes | str, data: bytes | str) -> bytes:
    if isinstance(key, str):
        key = key.encode("utf-8")
    if isinstance(data, str):
        data = data.encode("utf-8")
    if not key:
        raise ValueError("RC4 key cannot be empty")
    state = list(range(256))
    j = 0
    key_index = 0
    for i in range(256):
        j = (j + state[i] + key[key_index]) & 0xFF
        state[i], state[j] = state[j], state[i]
        key_index = (key_index + 1) % len(key)
    out = bytearray()
    i = j = 0
    for byte in data:
        i = (i + 1) & 0xFF
        j = (j + state[i]) & 0xFF
        state[i], state[j] = state[j], state[i]
        out.append(byte ^ state[(state[i] + state[j]) & 0xFF])
    return bytes(out)


def encrypt_param_with_fix(value: str, security_value: bytes | str) -> str:
    encrypted = rc4_ksa_prga(security_value, value.encode("utf-8"))
    return "ZTSP__" + base64.b64encode(encrypted).decode("ascii").replace("\n", "") + "__ZTSP"


def azeroth_canonical(
    method: str,
    path: str,
    params: Mapping[str, object | None],
    nonce: int | None = None,
) -> str:
    parts = [method.upper().strip(), path.strip()]
    for key in sorted(params):
        if not key.startswith("__"):
            parts.append(f"{key}={java_value(params[key])}")
    if nonce is not None:
        parts.append(str(nonce).strip())
    return "&".join(parts)


def _android_urlsafe_b64_no_wrap_no_padding(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")


def azeroth_nonce(now_ms: int | None = None, rand32: int | None = None) -> int:
    if now_ms is None:
        now_ms = int(time.time() * 1000)
    minute = int(now_ms // 60000) & 0xFFFFFFFF
    if rand32 is None:
        rand32 = random.getrandbits(32)
    # Java source is `this.a = i2 | (i3 << 32)` where i2/i3 are int.
    # Java masks int shift counts with 0x1f, so `i3 << 32` is actually
    # `i3 << 0`; the OR result is then sign-extended when assigned to long.
    value = (minute | (int(rand32) & 0xFFFFFFFF)) & 0xFFFFFFFF
    return value - 0x100000000 if value & 0x80000000 else value


def azeroth_hmac_client_sign(
    method: str,
    path: str,
    params: Mapping[str, object | None],
    security_b64: str,
    *,
    nonce: int | None = None,
) -> str:
    if not security_b64:
        return ""
    if nonce is None:
        nonce = azeroth_nonce()
    key = base64.b64decode(security_b64)
    canonical = azeroth_canonical(method, path, params, nonce).encode("utf-8")
    digest = hmac.new(key, canonical, hashlib.sha256).digest()
    return _android_urlsafe_b64_no_wrap_no_padding((nonce & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "big") + digest)


def load_all_zt_tables(assets_root: str | os.PathLike[str] = "resources/assets") -> dict[str, KwaiZtTable]:
    root = Path(assets_root) / "saio_res"
    tables: dict[str, KwaiZtTable] = {}
    for path in root.glob("zt_*.png"):
        table = decode_zt_png(path)
        tables[table.appkey] = table
    return tables

为什么算法层要单独拆出来

原因很简单:签名算法要可独立测试。REST 请求失败可能是账号态、风控、参数 profile、路径 rewrite、host、cookie 的问题;如果算法和请求发送混在一起,就无法定位失败到底来自哪里。

因此 algorithms.py 不发送请求,只负责纯函数:输入 bytes/map/table,输出 digest/signature/form 字段。


0x06 DFP / DID / EGID:不是一个普通接口,而是 KSecurity context 派生

DFP 这块如果只看 endpoint,很容易误以为就是一个普通 /rest/infra/... 表单。但 Java 初始化代码告诉我们,它先初始化 KSecurity,再设置 did/product/feature。

DFP 初始化:DFPInitUtils.java

  • 文件:sources/com/yxcorp/init/DFPInitUtils.java
  • 行数:162
  • 字节:5868
  • SHA256:1db13f0e1457f48eea3608e0d90f667d08cd4c3639c5027408d38f6e505e2c19

为什么看它:这里能看到 KSecurity.Initialize(...) 的 appkey/secret,以及 setDidsetProductNamesetWithFeature(ALL)

读完得到的结论:Python 的 DFP 表单构造不能脱离设备态;fetch_egid() / fetch_did() 只是最后发送层。

package com.yxcorp.init;

import cj.AppEnv;
import com.antman.utility.Log;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.KSecuritySdkILog;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.internal.common.KSecurityContext;
import com.yxcorp.buildconfig.BuildConfig;
import com.yxcorp.utils.antman.AntmanShellSPHelper;
import kotlin.jvm.internal.h;
import org.jetbrains.annotations.NotNull;

/* compiled from: DFPInitUtils.kt */
/* renamed from: com.yxcorp.init.d, reason: use source file name */
/* loaded from: classes.dex */
public final class DFPInitUtils {

    /* compiled from: DFPInitUtils.kt */
    /* renamed from: com.yxcorp.init.d$a */
    public static final class a implements KSecuritySdkILog {
        a() {
        }

        @Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
        public void onSecuriySuccess() {
        }

        @Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
        public void onSeucrityError(@NotNull KSException e3) {
            h.f(e3, "e");
        }

        @Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
        public void report(@NotNull String s2, @NotNull String s1) {
            h.f(s2, "s");
            h.f(s1, "s1");
        }
    }

    /* compiled from: DFPInitUtils.kt */
    /* renamed from: com.yxcorp.init.d$b */
    public static final class b implements KSecurityTrack.IKSecurityTrackCallback {

        /* compiled from: DFPInitUtils.kt */
        /* renamed from: com.yxcorp.init.d$b$a */
        public /* synthetic */ class a {
            public static final /* synthetic */ int[] a;

            static {
                int[] iArr = new int[KSecurityTrack.LEVEL.values().length];
                try {
                    iArr[KSecurityTrack.LEVEL.VERBOSE.ordinal()] = 1;
                } catch (NoSuchFieldError unused) {
                }
                try {
                    iArr[KSecurityTrack.LEVEL.DEBUG.ordinal()] = 2;
                } catch (NoSuchFieldError unused2) {
                }
                try {
                    iArr[KSecurityTrack.LEVEL.INFO.ordinal()] = 3;
                } catch (NoSuchFieldError unused3) {
                }
                try {
                    iArr[KSecurityTrack.LEVEL.WARN.ordinal()] = 4;
                } catch (NoSuchFieldError unused4) {
                }
                try {
                    iArr[KSecurityTrack.LEVEL.ERROR.ordinal()] = 5;
                } catch (NoSuchFieldError unused5) {
                }
                a = iArr;
            }
        }

        b() {
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public long getAppStartTime() {
            return -1L;
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public long getHomeStartTime() {
            return -1L;
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public int getLaunchSource() {
            return -1;
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        @NotNull
        public String getSessionId() {
            return "";
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public boolean isAppOnForeground() {
            return AppEnv.f1021m;
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public boolean isColdStart() {
            return AppEnv.f1020l;
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public void log(@NotNull KSecurityTrack.LEVEL level, @NotNull String s2, @NotNull String s1, @NotNull Throwable throwable) {
            h.f(level, "level");
            h.f(s2, "s");
            h.f(s1, "s1");
            h.f(throwable, "throwable");
            int i2 = a.a[level.ordinal()];
            if (i2 == 1) {
                Log.l(s2, s1, throwable);
                return;
            }
            if (i2 == 2) {
                Log.c(s2, s1, throwable);
                return;
            }
            if (i2 == 3) {
                Log.g(s2, s1, throwable);
                return;
            }
            if (i2 == 4) {
                Log.n(s2, s1, throwable);
            } else if (i2 != 5) {
                Log.g(s2, s1, throwable);
            } else {
                Log.e(s2, s1, throwable);
            }
        }

        @Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
        public void logsdkReport(@NotNull String s2, @NotNull String s1) {
            h.f(s2, "s");
            h.f(s1, "s1");
            LogManagerInitUtil.G().a(s2, s1);
        }
    }

    public static final void a() {
        try {
            KSecurity.Initialize(AppEnv.a(), "d7b7d042-d4f2-4012-be60-d97ff2429c17", "lD6We1E8i", new a());
        } catch (KSException e3) {
            e3.printStackTrace();
        }
        b();
        KSecurity.getkSecurityParameterContext().setDid(AntmanShellSPHelper.a.e());
        KSecurity.getkSecurityParameterContext().setProductName(BuildConfig.BIG_KPN);
        KSecurity.getkSecurityParameterContext().setWithFeature(KSecurityContext.Feature.ALL);
    }

    private static final void b() {
        KSecurityTrack.setDelegate(new b());
    }
}

DFP 对照实现

DFP 的实现没有单独放一个新文件,而是在 algorithms.py 里构造字段,在 client.py 里发请求。等会儿 client.py 完整内联时可以看到 DFP_EGID_ENDPOINTDFP_DID_ENDPOINTbuild_dfp_form()fetch_egid()fetch_did()


0x07 Azeroth:另一套签名模型,不要和主 App sig3 混用

Azeroth 是另一条线。主 App __NS_sig3path + sig,但 Azeroth 的 Java 证据显示它走 method + path + params canonical,再交给 MXSec wrapper。

Azeroth 参数处理:AzerothParamProcessor.java

  • 文件:sources/vc/AzerothParamProcessor.java
  • 行数:202
  • 字节:7441
  • SHA256:5774a8817a407e34f54e086a0802ca8b7feba7d99f79eb2daec6442bce79f00c

为什么看它:它明确读取 request.method()request.url().h() 和 params,再调用 SignatureUtil.d() / MXSec wrapper。

读完得到的结论:Python 对应 azeroth_canonical()azeroth_nonce()azeroth_hmac_client_sign(),不能复用主 App 的 path + sig

package vc;

import ad.EncryptParamHandler;
import android.os.SystemClock;
import com.kuaishou.dfp.c.k;
import com.kuaishou.weapon.ks.x;
import com.kwai.middleware.azeroth.Azeroth;
import com.kwai.middleware.azeroth.Azeroth2;
import com.kwai.middleware.leia.handler.LeiaParamProcessor;
import java.util.LinkedHashMap;
import java.util.Map;
import kotlin.jvm.internal.Ref$ObjectRef;
import kotlin.jvm.internal.h;
import mh.MXSec;
import mh.MXWrapper;
import okhttp3.Request;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import qd.SignatureUtil;
import td.NetExt;

/* compiled from: AzerothParamProcessor.kt */
/* renamed from: vc.c, reason: use source file name */
/* loaded from: classes.dex */
public class AzerothParamProcessor extends LeiaParamProcessor {
    private String b;
    private final AzerothParamExtractor c;

    /* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
    public AzerothParamProcessor(@NotNull AzerothParamExtractor azerothExtractor) {
        super(azerothExtractor);
        h.g(azerothExtractor, "azerothExtractor");
        this.c = azerothExtractor;
    }

    @Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
    @NotNull
    public Map<String, String> a() {
        LinkedHashMap linkedHashMap = new LinkedHashMap();
        linkedHashMap.put("Accept-Language", d().h());
        linkedHashMap.put("X-REQUESTID", String.valueOf(SystemClock.elapsedRealtime()));
        linkedHashMap.put("Connection", "keep-alive");
        String n2 = n(i());
        if (n2 != null) {
            if (n2.length() > 0) {
                linkedHashMap.put("Cookie", n2);
            }
        }
        return linkedHashMap;
    }

    @Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
    @NotNull
    public Map<String, String> b() {
        return super.b();
    }

    @Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
    @NotNull
    public Map<String, String> c() {
        LinkedHashMap linkedHashMap = new LinkedHashMap();
        linkedHashMap.put("kpn", d().m());
        linkedHashMap.put("kpf", d().l());
        linkedHashMap.put("appver", d().b());
        linkedHashMap.put("ver", d().c());
        linkedHashMap.put("gid", d().g());
        String f = d().f();
        if (f.length() == 0) {
            throw new IllegalArgumentException("The device id cannot be null or empty.");
        }
        linkedHashMap.put("did", f);
        linkedHashMap.put("userId", d().r());
        if (td.b.e(Azeroth2.f2692s.j(), k.f2103j)) {
            String valueOf = String.valueOf(d().i());
            String valueOf2 = String.valueOf(d().j());
            if (this.c.B()) {
                valueOf = EncryptParamHandler.c(valueOf);
                h.b(valueOf, "EncryptParamHandler.encryptWithFix(latitude)");
                valueOf2 = EncryptParamHandler.c(valueOf2);
                h.b(valueOf2, "EncryptParamHandler.encryptWithFix(longitude)");
            }
            linkedHashMap.put("lat", valueOf);
            linkedHashMap.put("lon", valueOf2);
        }
        linkedHashMap.put("mod", d().k());
        linkedHashMap.put("net", NetExt.c(d().a()));
        linkedHashMap.put("os", "android");
        linkedHashMap.put(x.f2587p, d().d());
        linkedHashMap.put("language", d().h());
        linkedHashMap.put("countryCode", d().e());
        linkedHashMap.put("sys", d().q());
        String str = this.b;
        if (str != null) {
            linkedHashMap.put("subBiz", str);
        }
        return linkedHashMap;
    }

    @Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
    public void g(@NotNull String path, @NotNull Map<String, String> urlParams) {
        h.g(path, "path");
        h.g(urlParams, "urlParams");
        Azeroth c = Azeroth.c();
        h.b(c, "Azeroth.get()");
        lc.h g3 = c.g();
        h.b(g3, "Azeroth.get().initParams");
        g3.c().a(path, urlParams);
    }

    /* JADX WARN: Multi-variable type inference failed */
    /* JADX WARN: Type inference failed for: r2v3, types: [T, java.lang.String] */
    @Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
    @NotNull
    public Map<String, String> h(@NotNull Request request, @NotNull Map<String, String> params) {
        h.g(request, "request");
        h.g(params, "params");
        LinkedHashMap linkedHashMap = new LinkedHashMap();
        Ref$ObjectRef ref$ObjectRef = new Ref$ObjectRef();
        ?? f = f(request, params, Azeroth2.f2692s.v().n());
        ref$ObjectRef.element = f;
        if (f.length() > 0) {
            linkedHashMap.put("__clientSign", (String) ref$ObjectRef.element);
        }
        if (this.c.t()) {
            String k3 = k(request, params);
            if (!(k3.length() > 0)) {
                throw new IllegalStateException("麻烦联系安全组张艳生,升级或者接入KWSecuritySDK:3.9.1.4 + 版本,以便完成__NS_sig3的计算流程");
            }
            linkedHashMap.put("__NS_sig3", k3);
        }
        return linkedHashMap;
    }

    @NotNull
    public Map<String, String> i() {
        LinkedHashMap linkedHashMap = new LinkedHashMap();
        String p2 = d().p();
        String o2 = d().o();
        linkedHashMap.put("did", d().f());
        if (p2.length() > 0) {
            if (o2.length() > 0) {
                linkedHashMap.put(o2 + "_st", p2);
            }
        }
        return linkedHashMap;
    }

    @NotNull
    public String j(@NotNull String method, @NotNull String path, @NotNull Map<String, String> params) {
        h.g(method, "method");
        h.g(path, "path");
        h.g(params, "params");
        String d = SignatureUtil.d(method, path, params, null);
        MXSec a = MXSec.a();
        h.b(a, "MXSec.get()");
        MXWrapper d3 = a.d();
        Azeroth2 azeroth2 = Azeroth2.f2692s;
        String a2 = d3.a("azeroth", "010a11c6-f2cb-4016-887d-0d958aef1534", 0, d);
        h.b(a2, "MXSec.get().mxWrapper.at…stringThatNeedToBeSigned)");
        return a2;
    }

    @NotNull
    public String k(@NotNull Request request, @NotNull Map<String, String> params) {
        h.g(request, "request");
        h.g(params, "params");
        String method = request.method();
        h.b(method, "request.method()");
        String h3 = request.url().h();
        h.b(h3, "request.url().encodedPath()");
        return j(method, h3, params);
    }

    @Nullable
    protected final String l() {
        return this.b;
    }

    public final void m(@NotNull String subBiz) {
        h.g(subBiz, "subBiz");
        this.b = subBiz;
    }

    @Nullable
    protected final String n(@NotNull Map<String, String> cookieMap) {
        h.g(cookieMap, "cookieMap");
        if (cookieMap.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : cookieMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            sb.append(key);
            sb.append('=');
            sb.append(value);
            sb.append(";");
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }
}

Azeroth 配置接口:AzerothService.java

  • 文件:sources/com/kwai/middleware/azeroth/api/AzerothService.java
  • 行数:25
  • 字节:1484
  • SHA256:0582fc9705615d4ffbfa0f565e9695e9e7e604c84bb9e59b34dbcfc5ba59d234

为什么看它:它给出 /rest/zt/appsupport/configs 的 Retrofit 注解来源。

读完得到的结论:Python RestEndpoint 里的 azeroth_configs 与它对应。

package com.kwai.middleware.azeroth.api;

import com.google.gson.JsonObject;
import com.kuaishou.weapon.ks.x;
import io.reactivex.Observable;
import java.util.Map;
import kotlin.Metadata;
import kotlin.jvm.JvmSuppressWildcards;
import org.jetbrains.annotations.NotNull;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.QueryMap;
import wc.AzerothResponse;

/* compiled from: AzerothService.kt */
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000\"\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010$\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\bf\u0018\u00002\u00020\u0001J*\u0010\b\u001a\u000e\u0012\n\u0012\b\u0012\u0004\u0012\u00020\u00070\u00060\u00052\u0014\b\u0001\u0010\u0004\u001a\u000e\u0012\u0004\u0012\u00020\u0003\u0012\u0004\u0012\u00020\u00010\u0002H'¨\u0006\t"}, d2 = {"Lcom/kwai/middleware/azeroth/api/a;", "", "", "", "extraParams", "Lio/reactivex/n;", "Lwc/b;", "Lcom/google/gson/JsonObject;", x.f2583l, "azeroth_release"}, k = 1, mv = {1, 4, 0})
/* renamed from: com.kwai.middleware.azeroth.api.a, reason: use source file name */
/* loaded from: classes.dex */
public interface AzerothService {
    @GET("/rest/zt/appsupport/configs")
    @JvmSuppressWildcards
    @NotNull
    @Headers({"Content-Type:application/x-www-form-urlencoded; charset=utf-8"})
    Observable<AzerothResponse<JsonObject>> a(@QueryMap @NotNull Map<String, Object> extraParams);
}

0x08 REST 客户端:把算法接到真实请求 shape

算法能跑不代表请求能跑。请求还需要:host、path rewrite、GET/POST 参数位置、auth 字段、dynamic capture、HAR 导入、私信/搜索/视频/用户/消息 wrapper。所有这些集中在 client.py

协议层完整实现:kws_recovered/client.py

  • 文件:kws_recovered/client.py
  • 行数:3318
  • 字节:137308
  • SHA256:4d13369551b7e2d8d1158876ca3876abcce7f1d32043319153dca095c96e8459

为什么看它:这是项目里最核心的工程文件:设备状态、REST endpoint、签名请求、HAR 导入、登录态、动态字段捕获、搜索/视频/用户/消息/私信 wrapper 都在这里。

读完得到的结论:它把 ParamsUtilsKwaiParams、HAR observed、IDA/Python 算法串成可调用的客户端。

from __future__ import annotations

import base64
import socket
import hashlib
import json
import re
import time
import urllib.parse
import uuid
from contextlib import contextmanager
from dataclasses import dataclass, field, fields, replace
from http.cookies import SimpleCookie
from pathlib import Path
from typing import Any, Iterable, Mapping

from .dex_annotations import extract_retrofit_endpoints
from .algorithms import (
    APPKEY_AZEROTH,
    APPKEY_MAIN,
    KwaiZtTable,
    azeroth_canonical,
    azeroth_hmac_client_sign,
    build_lite_dfp,
    build_lite_dfp_fields,
    encrypt_param_with_fix,
    encode_dfp_proto,
    find_zt_table,
    kwai_sig,
    kwai_token_sig,
    sig3_10405,
    sig3_10417,
    sig3_10418,
    zt_atlas_encrypt_like,
)


DFP_APPKEY = APPKEY_MAIN
DFP_PRODUCT_NAME = "KUAISHOU"
DFP_APP_VERSION = "10.6.30.12345"
DFP_EGID_ENDPOINT = "https://gdfpsec.ksapisrv.com/rest/infra/gdfp/report/kuaishou/android"
DFP_DID_ENDPOINT = "https://gdfpsec.ksapisrv.com/rest/infra/unifiedId/fetch/android"
DNS_OVERRIDE_IPS = {
    "gdfpsec.ksapisrv.com": (
        "124.156.126.37",
        "103.107.217.68",
        "103.102.202.85",
        "103.102.200.2",
        "103.107.217.28",
    ),
}

CAPTCHA_VERIFY_ENDPOINT = "https://app.m.kuaishou.com/rest/wd/captcha/verify"
CAPTCHA_IFRAME_BASE = "https://captcha.zt.kuaishou.com/iframe/index.html"

MAIN_VIDEO_LEGACY_APPVER = "6.5.5.9591"
MAIN_VIDEO_LEGACY_VER = "6.5"
MAIN_VIDEO_LOGIN_APPVER = "6.9.9.9999"
MAIN_VIDEO_LOGIN_VER = "6.9"
MAIN_VIDEO_MAX_MEMORY = "192"

AZEROTH_HOST = "https://api.kuaishouzt.com"
AZEROTH_LATEST_APPVER = "14.3.30.47512"
AZEROTH_LATEST_VER = "14.3"
AZEROTH_ANDROID_API_LEVEL = "31"


@dataclass(frozen=True)
class CaptchaChallenge:
    error_url: str
    key: str
    verify_type: str
    uri: str
    captcha_session: str = ""
    iframe_type: str = ""
    config_url: str = ""

    @property
    def iframe_url(self) -> str:
        if not self.captcha_session or not self.config_url:
            return ""
        query = urllib.parse.urlencode(
            {
                "captchaSession": self.captcha_session,
                "type": self.iframe_type or "1",
                "configUrl": self.config_url,
            }
        )
        return f"{CAPTCHA_IFRAME_BASE}?{query}"


def _first_query_value(query: Mapping[str, list[str]], key: str) -> str:
    values = query.get(key) or [""]
    return values[0] if values else ""


def _regex_group(pattern: str, text: str, default: str = "") -> str:
    match = re.search(pattern, text, re.S)
    return match.group(1) if match else default


def parse_captcha_challenge(error_url: str, page_html: str = "") -> CaptchaChallenge:
    parsed = urllib.parse.urlsplit(error_url)
    query = urllib.parse.parse_qs(parsed.query)
    key = _first_query_value(query, "key") or _regex_group(r'id=["\']captcha-key["\'][^>]*value=["\']([^"\']+)', page_html)
    verify_type = _first_query_value(query, "type")
    uri = urllib.parse.unquote(_first_query_value(query, "uri"))
    captcha_body = _regex_group(r"window\.kwaiCaptchaData\s*=\s*\{(.*?)\}\s*</script>", page_html)
    captcha_session = _regex_group(r'captchaSession\s*:\s*"([^"]+)"', captcha_body)
    iframe_type = _regex_group(r'type\s*:\s*([0-9]+)', captcha_body)
    config_url = _regex_group(r'configUrl\s*:\s*"([^"]+)"', captcha_body)
    return CaptchaChallenge(
        error_url=error_url,
        key=key,
        verify_type=verify_type,
        uri=uri,
        captcha_session=captcha_session,
        iframe_type=iframe_type,
        config_url=config_url,
    )


def build_captcha_manual_html(challenge: CaptchaChallenge) -> str:
    payload = json.dumps(
        {
            "error_url": challenge.error_url,
            "key": challenge.key,
            "verify_type": challenge.verify_type,
            "uri": challenge.uri,
            "captcha_session": challenge.captcha_session,
            "iframe_type": challenge.iframe_type,
            "config_url": challenge.config_url,
            "iframe_url": challenge.iframe_url,
        },
        ensure_ascii=False,
    )
    return """<!doctype html>
<html lang=\"zh-CN\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">
<title>Kwai captcha manual helper</title>
<style>
body{{font-family:system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;margin:24px;background:#f6f7f8;color:#111}}
main{{max-width:760px;margin:auto;background:#fff;border-radius:12px;padding:18px;box-shadow:0 8px 30px rgba(0,0,0,.08)}}
iframe{{width:100%;height:420px;border:1px solid #ddd;border-radius:10px;background:#fff}}
pre{{white-space:pre-wrap;word-break:break-all;background:#111;color:#d7ffd7;border-radius:8px;padding:12px}}
.muted{{color:#666}}
</style>
</head>
<body>
<main>
<h2>Kwai captcha manual helper</h2>
<p class=\"muted\">Solve the slider captcha. This page will show the iframe ticket; pass it to <code>captcha-verify</code> to get the final <code>captcha_token</code>.</p>
<iframe id=\"captcha\" src=\"{iframe_url}\"></iframe>
<h3>Challenge</h3>
<pre id=\"challenge\"></pre>
<h3>Iframe result / ticket</h3>
<pre id=\"result\">Waiting for captcha result...</pre>
<h3>Verify command</h3>
<pre id=\"command\">Waiting for ticket...</pre>
</main>
<script>
const challenge = {payload};
document.getElementById('challenge').textContent = JSON.stringify(challenge, null, 2);
function quoteArg(value) {{ return '\"' + String(value).replace(/\"/g, '\\\"') + '\"'; }}
window.addEventListener('message', function(event) {{
  let data = event.data;
  try {{ if (typeof data === 'string') data = JSON.parse(data); }} catch (e) {{ return; }}
  if (!data || data.msgType !== 'RESULT') return;
  const msg = data.msg || {{}};
  const ticket = msg.ticket || msg.token || msg.captchaToken || '';
  document.getElementById('result').textContent = JSON.stringify(data, null, 2);
  if (ticket) {{
    const cmd = 'python kws_client.py captcha-verify --error-url ' + quoteArg(challenge.error_url) + ' --ticket ' + quoteArg(ticket);
    document.getElementById('command').textContent = cmd;
    navigator.clipboard && navigator.clipboard.writeText(ticket).catch(function(){{}});
  }}
}});
</script>
</body>
</html>
""".format(iframe_url=challenge.iframe_url, payload=payload)

DEVICE_FIELD_ALIASES = {
    "did": ("did", "cloud_did", "deviceId", "device_id", "antman_shell_device_id"),
    "cdid_tag": ("cdid_tag", "did_tag", "cloud_did_tag", "cloudDeviceIdTag", "antman_shell_cloud_id_tag"),
    "rdid": ("rdid", "randomDeviceId", "random_device_id", "antman_shell_random_device_id"),
    "odid": ("odid", "old_did", "origin_did", "originalDid"),
    "decision_did": ("decisionDid", "decision_did"),
    "decision": ("decision", "decisionType", "decision_type"),
    "decision_inputs": ("decisionInputs", "decision_inputs"),
    "egid": ("egid", "EGID"),
    "token": ("token", "gifshow_token", "gifshowToken"),
    "api_st": ("kuaishou.api_st", "api_st", "apiSt", "nebula_token"),
    "client_salt": ("client_salt", "clientSalt", "token_client_salt", "kuaishou.api_client_salt"),
    "service_token": ("serviceToken", "service_token", "azeroth_service_token"),
    "azeroth_security": ("security", "azeroth_security", "clientSecurity"),
    "ud": ("ud", "user_id", "userId", "gifshow_userid"),
    "iuid": ("iuid", "iUserId"),
    "did_gt": ("did_gt", "new_device_install_app_time"),
}
SAFE_RESPONSE_FIELD_ALIASES = {
    **DEVICE_FIELD_ALIASES,
    "ud": ("ud", "gifshow_userid"),
}
DEVICE_ALIAS_NAMES = {alias for aliases in DEVICE_FIELD_ALIASES.values() for alias in aliases}
CAPTURE_REQUEST_FIELD_ALIASES = {
    **DEVICE_FIELD_ALIASES,
    "appver": ("appver", "appVer"),
    "channel": ("c", "channel"),
    "country_code": ("country_code", "countryCode"),
    "language": ("language",),
    "mod": ("mod", "deviceName"),
    "net": ("net",),
    "new_oc": ("newOc", "new_oc"),
    "oc": ("oc",),
    "sys": ("sys", "sysver"),
}
NON_LOGIN_TOKEN_MARKERS = ("captcha", "verification", "ztbasic", "zt.basic")


def _iter_mappings(value: Any) -> Iterable[Mapping[str, Any]]:
    if isinstance(value, Mapping):
        yield value
        for nested in value.values():
            yield from _iter_mappings(nested)
    elif isinstance(value, list):
        for item in value:
            yield from _iter_mappings(item)


def _high_confidence_response_mappings(value: Any) -> Iterable[Mapping[str, Any]]:
    if not isinstance(value, Mapping):
        return
    yield value
    for key in ("data", "result", "tokenInfo", "loginInfo", "user", "userInfo"):
        nested = value.get(key)
        if isinstance(nested, Mapping):
            yield nested


def _response_json(response: Any) -> Any:
    try:
        return response.json()
    except Exception:
        text = getattr(response, "text", "") or ""
        if not text:
            return None
        try:
            return json.loads(text)
        except Exception:
            return None


def _response_cookies(response: Any) -> dict[str, str]:
    cookies: dict[str, str] = {}
    jar = getattr(response, "cookies", None)
    if jar is not None:
        try:
            cookies.update({str(key): str(value) for key, value in jar.get_dict().items() if value})
        except Exception:
            try:
                cookies.update({str(cookie.name): str(cookie.value) for cookie in jar if cookie.value})
            except Exception:
                pass
    headers = getattr(response, "headers", None) or {}
    set_cookie = ""
    try:
        set_cookie = headers.get("Set-Cookie", "") or headers.get("set-cookie", "") or ""
    except Exception:
        set_cookie = ""
    if set_cookie:
        parsed = SimpleCookie()
        try:
            parsed.load(set_cookie)
            cookies.update({key: morsel.value for key, morsel in parsed.items() if morsel.value})
        except Exception:
            pass
    return cookies


SENSITIVE_STATE_FIELDS = {
    "token",
    "api_st",
    "client_salt",
    "service_token",
    "azeroth_security",
    "account_private_key_pem",
}
SENSITIVE_COOKIE_NAMES = {
    "token",
    "kuaishou.api_st",
    "api_st",
    "client_salt",
    "clientSalt",
    "serviceToken",
    "service_token",
}


def _redact_value(value: Any) -> Any:
    if value in (None, "", {}, []):
        return value
    text = str(value)
    if len(text) <= 8:
        return "<set>"
    return f"<set:{len(text)} chars:{text[:4]}...{text[-4:]}>"


def redact_state_mapping(mapping: Mapping[str, Any]) -> dict[str, Any]:
    redacted: dict[str, Any] = {}
    for key, value in mapping.items():
        if key in SENSITIVE_STATE_FIELDS:
            redacted[key] = _redact_value(value)
        elif key == "cookie_extras" and isinstance(value, Mapping):
            redacted[key] = {
                str(cookie_key): _redact_value(cookie_value)
                if str(cookie_key) in SENSITIVE_COOKIE_NAMES
                else cookie_value
                for cookie_key, cookie_value in value.items()
            }
        else:
            redacted[key] = value
    return redacted


def _parse_cookie_header_value(cookie_header: Any) -> dict[str, str]:
    if cookie_header is None:
        return {}
    text = str(cookie_header).strip()
    if not text:
        return {}
    cookies: dict[str, str] = {}
    parsed = SimpleCookie()
    try:
        parsed.load(text)
        cookies.update({key: morsel.value for key, morsel in parsed.items() if morsel.value})
    except Exception:
        pass
    if cookies:
        return cookies
    for item in text.split(";"):
        if "=" not in item:
            continue
        key, value = item.split("=", 1)
        key = key.strip()
        value = value.strip()
        if key and value:
            cookies[key] = value
    return cookies


def _header_items_from_value(value: Any) -> Iterable[tuple[str, str]]:
    if isinstance(value, Mapping):
        for key, item in value.items():
            if item is not None:
                yield str(key), str(item)
    elif isinstance(value, list):
        for item in value:
            if not isinstance(item, Mapping):
                continue
            name = item.get("name") or item.get("key")
            header_value = item.get("value") or item.get("val")
            if name is not None and header_value is not None:
                yield str(name), str(header_value)


def _name_value_pairs_from_value(value: Any) -> dict[str, str]:
    pairs: dict[str, str] = {}
    if isinstance(value, Mapping):
        for key, item in value.items():
            if item is not None:
                pairs[str(key)] = str(item)
    elif isinstance(value, list):
        for item in value:
            if not isinstance(item, Mapping):
                continue
            name = item.get("name") or item.get("key")
            pair_value = item.get("value") or item.get("val")
            if name is not None and pair_value is not None:
                pairs[str(name)] = str(pair_value)
    return pairs


def _capture_json_payload(text: str) -> Any:
    stripped = text.lstrip("\ufeff").strip()
    if not stripped or stripped[0] not in "[{":
        return None
    try:
        return json.loads(stripped)
    except Exception:
        return None


def _coerce_capture_payload(capture: Any) -> tuple[Any, str]:
    if isinstance(capture, bytes):
        text = capture.decode("utf-8", errors="replace")
        return _capture_json_payload(text), text
    if isinstance(capture, str):
        return _capture_json_payload(capture), capture
    if isinstance(capture, (Mapping, list)):
        try:
            text = json.dumps(capture, ensure_ascii=False)
        except Exception:
            text = ""
        return capture, text
    return None, "" if capture is None else str(capture)


def _decode_token_probe(value: Any) -> str:
    text = urllib.parse.unquote_plus(str(value or "")).strip()
    if not text:
        return ""
    compact = re.sub(r"[^A-Za-z0-9_=-]", "", text[:1024])
    if len(compact) < 8:
        return ""
    padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
    try:
        return base64.urlsafe_b64decode(padded.encode("ascii", errors="ignore")).decode("utf-8", errors="ignore")
    except Exception:
        return ""


def _looks_like_non_login_token(key: str, value: Any) -> bool:
    if key != "token":
        return False
    text = str(value or "")
    probe = (text[:256] + " " + _decode_token_probe(text)[:512]).lower()
    return any(marker in probe for marker in NON_LOGIN_TOKEN_MARKERS)


def _filter_capture_auth_pairs(pairs: Mapping[str, str]) -> dict[str, str]:
    return {
        str(key): str(value)
        for key, value in pairs.items()
        if value is not None and not _looks_like_non_login_token(str(key), value)
    }


def _filter_capture_mapping(mapping: Mapping[str, Any]) -> dict[str, Any]:
    return {
        str(key): value
        for key, value in mapping.items()
        if value is not None and not _looks_like_non_login_token(str(key), value)
    }


def _urlencoded_pairs_from_text(text: str) -> dict[str, str]:
    if "=" not in text:
        return {}
    try:
        return {str(key): str(value) for key, value in urllib.parse.parse_qsl(text, keep_blank_values=True)}
    except Exception:
        return {}


def _is_har_payload(payload: Any) -> bool:
    return isinstance(payload, Mapping) and (
        isinstance(payload.get("log"), Mapping) and isinstance(payload.get("log", {}).get("entries"), list)
        or isinstance(payload.get("entries"), list)
    )


def _request_pairs_from_capture_payload(payload: Any) -> dict[str, str]:
    pairs: dict[str, str] = {}
    for mapping in _iter_mappings(payload):
        url = mapping.get("url")
        if isinstance(url, str) and url:
            pairs.update(dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(url).query, keep_blank_values=True)))
        for key in ("queryString", "query", "params", "form", "data", "body"):
            value = mapping.get(key)
            if isinstance(value, str):
                pairs.update(_urlencoded_pairs_from_text(value))
            else:
                pairs.update(_name_value_pairs_from_value(value))
        post_data = mapping.get("postData")
        if isinstance(post_data, Mapping):
            pairs.update(_name_value_pairs_from_value(post_data.get("params")))
            text = post_data.get("text")
            if isinstance(text, str):
                pairs.update(_urlencoded_pairs_from_text(text))
    return _filter_capture_auth_pairs(pairs)


def _auth_pairs_from_text(text: str) -> dict[str, str]:
    pairs: dict[str, str] = {}
    alias_names = DEVICE_ALIAS_NAMES | {"kuaishou.api_st", "api_st", "client_salt", "clientSalt"}
    pattern = re.compile(r"(?<![\w.-])([A-Za-z0-9_.-]{2,48})=([^&;\s'\"<>]+)")
    for match in pattern.finditer(text):
        key = urllib.parse.unquote_plus(match.group(1))
        if key not in alias_names:
            continue
        value = urllib.parse.unquote_plus(match.group(2))
        if value:
            pairs[key] = value
    json_pair_pattern = re.compile(r'''["']([A-Za-z0-9_.-]{2,48})["']\s*:\s*["']([^"']+)["']''')
    for match in json_pair_pattern.finditer(text):
        key = match.group(1)
        if key in alias_names:
            pairs[key] = match.group(2)
    return _filter_capture_auth_pairs(pairs)


def _cookies_from_capture_payload(payload: Any) -> dict[str, str]:
    cookies: dict[str, str] = {}
    for mapping in _iter_mappings(payload):
        for key in ("Cookie", "cookie", "Set-Cookie", "set-cookie"):
            if key in mapping:
                cookies.update(_parse_cookie_header_value(mapping.get(key)))
        for header_name, header_value in _header_items_from_value(mapping.get("headers")):
            if header_name.lower() in {"cookie", "set-cookie"}:
                cookies.update(_parse_cookie_header_value(header_value))
        cookie_value = mapping.get("cookies")
        if isinstance(cookie_value, str):
            cookies.update(_parse_cookie_header_value(cookie_value))
        else:
            cookies.update(_name_value_pairs_from_value(cookie_value))
    return cookies


def _auth_pairs_from_capture_payload(payload: Any) -> dict[str, str]:
    pairs: dict[str, str] = {}
    for mapping in _iter_mappings(payload):
        for key in ("queryString", "query", "params", "form", "data", "body", "postData"):
            value = mapping.get(key)
            if isinstance(value, str):
                pairs.update(_auth_pairs_from_text(value))
            else:
                pairs.update(_name_value_pairs_from_value(value))
        post_data = mapping.get("postData")
        if isinstance(post_data, Mapping):
            params = post_data.get("params")
            pairs.update(_name_value_pairs_from_value(params))
            text = post_data.get("text")
            if isinstance(text, str):
                pairs.update(_auth_pairs_from_text(text))
    return {key: value for key, value in pairs.items() if key in DEVICE_ALIAS_NAMES or key in {"kuaishou.api_st", "api_st", "client_salt", "clientSalt"}}


def _capture_text_values(payload: Any) -> Iterable[str]:
    for mapping in _iter_mappings(payload):
        for key in ("text", "body", "responseBody", "raw", "content"):
            value = mapping.get(key)
            if not isinstance(value, str) or not value:
                continue
            if key == "text" and str(mapping.get("encoding", "")).lower() == "base64":
                try:
                    yield base64.b64decode(value).decode("utf-8", errors="replace")
                    continue
                except Exception:
                    pass
            yield value


def _json_mappings_from_capture_texts(payload: Any) -> Iterable[Mapping[str, Any]]:
    for text in _capture_text_values(payload):
        parsed = _capture_json_payload(text)
        if isinstance(parsed, Mapping):
            yield parsed


def _json_mappings_from_raw_text(text: str) -> Iterable[Mapping[str, Any]]:
    decoder = json.JSONDecoder()
    text = text.lstrip("\ufeff")
    for index, char in enumerate(text):
        if char not in "[{":
            continue
        try:
            parsed, _ = decoder.raw_decode(text[index:])
        except Exception:
            continue
        for mapping in _iter_mappings(parsed):
            yield mapping
        return


def _cookies_from_raw_capture_text(text: str) -> dict[str, str]:
    cookies: dict[str, str] = {}
    header_pattern = re.compile(r"(?im)^\s*(?:Cookie|Set-Cookie)\s*:\s*(.+)$")
    for match in header_pattern.finditer(text):
        cookies.update(_parse_cookie_header_value(match.group(1)))
    curl_header_pattern = re.compile(r"(?is)-H\s+(['\"])(Cookie|Set-Cookie):\s*(.*?)\1")
    for match in curl_header_pattern.finditer(text):
        cookies.update(_parse_cookie_header_value(match.group(3)))
    return cookies


def import_login_capture_into_device(
    device: "KwaiDeviceContext",
    capture: Any,
    *,
    overwrite: bool = True,
) -> dict[str, str]:
    payload, raw_text = _coerce_capture_payload(capture)
    changed: dict[str, str] = {}
    is_har = _is_har_payload(payload)
    if payload is not None:
        if is_har:
            request_pairs = _request_pairs_from_capture_payload(payload)
            if request_pairs:
                changed.update(
                    device.update_from_mapping(
                        request_pairs,
                        overwrite=overwrite,
                        aliases=CAPTURE_REQUEST_FIELD_ALIASES,
                        recursive=False,
                    )
                )
        else:
            changed.update(device.update_from_mapping(payload, overwrite=overwrite))
        cookie_map = _filter_capture_auth_pairs(_cookies_from_capture_payload(payload))
        if cookie_map:
            changed.update(device.update_from_mapping(cookie_map, overwrite=overwrite, recursive=False))
            device.cookie_extras.update({key: value for key, value in cookie_map.items() if key not in DEVICE_ALIAS_NAMES and value})
        auth_pairs = _auth_pairs_from_capture_payload(payload)
        if auth_pairs:
            changed.update(device.update_from_mapping(auth_pairs, overwrite=overwrite, recursive=False))
    if raw_text and not is_har:
        cookie_map = _filter_capture_auth_pairs(_cookies_from_raw_capture_text(raw_text))
        if cookie_map:
            changed.update(device.update_from_mapping(cookie_map, overwrite=overwrite, recursive=False))
            device.cookie_extras.update({key: value for key, value in cookie_map.items() if key not in DEVICE_ALIAS_NAMES and value})
        auth_pairs = _auth_pairs_from_text(raw_text)
        if auth_pairs:
            changed.update(device.update_from_mapping(auth_pairs, overwrite=overwrite, recursive=False))
        for parsed_mapping in _json_mappings_from_raw_text(raw_text):
            changed.update(device.update_from_mapping(parsed_mapping, overwrite=overwrite))
    if payload is not None:
        for parsed_mapping in _json_mappings_from_capture_texts(payload):
            if is_har:
                auth_response = any(
                    key in parsed_mapping
                    for key in ("token", "api_st", "kuaishou.api_st", "clientSalt", "client_salt", "tokenInfo", "loginInfo")
                )
                response_aliases = DEVICE_FIELD_ALIASES if auth_response else SAFE_RESPONSE_FIELD_ALIASES
                for response_mapping in _high_confidence_response_mappings(parsed_mapping):
                    filtered_mapping = _filter_capture_mapping(response_mapping)
                    changed.update(
                        device.update_from_mapping(
                            filtered_mapping,
                            overwrite=overwrite,
                            aliases=response_aliases,
                            recursive=False,
                        )
                    )
            else:
                filtered_mapping = _filter_capture_mapping(parsed_mapping)
                changed.update(device.update_from_mapping(filtered_mapping, overwrite=overwrite))
                nested_auth_pairs = _auth_pairs_from_capture_payload(filtered_mapping)
                if nested_auth_pairs:
                    changed.update(device.update_from_mapping(nested_auth_pairs, overwrite=overwrite, recursive=False))
    return changed


def _android_base64_default(data: bytes) -> str:
    # Android Base64.DEFAULT wraps at 76 chars and appends a trailing LF.
    return base64.encodebytes(data).decode("ascii")


def _dfp_encode_payload(data: bytes) -> str:
    return urllib.parse.quote(_android_base64_default(data), safe="")


def _dfp_sign_value(data: bytes, table: KwaiZtTable | None, sign_mode: str, *, inner: bool = True) -> str:
    if sign_mode == "none":
        return ""
    if sign_mode == "sha256":
        return hashlib.sha256(data).hexdigest()
    if sign_mode == "double-sha256":
        return hashlib.sha256(hashlib.sha256(data).hexdigest().encode("utf-8")).hexdigest()
    if sign_mode == "10405":
        from .algorithms import sig3_10405

        return sig3_10405(data, table=table)
    if sign_mode == "10418-sha256":
        return sig3_10417(hashlib.sha256(data).hexdigest().encode("utf-8"), table)
    if sign_mode == "10417":
        return sig3_10417(data, table)
    if sign_mode == "10418":
        if table is None:
            return ""
        return sig3_10418(data, table, inner=inner)
    raise ValueError(f"unsupported DFP sign mode: {sign_mode}")


@contextmanager
def _temporary_dns_override(hostname: str, ip_address: str):
    original_getaddrinfo = socket.getaddrinfo

    def patched_getaddrinfo(host: str, port: int, *args: Any, **kwargs: Any) -> Any:
        if host == hostname:
            host = ip_address
        return original_getaddrinfo(host, port, *args, **kwargs)

    socket.getaddrinfo = patched_getaddrinfo
    try:
        yield
    finally:
        socket.getaddrinfo = original_getaddrinfo


def _is_dns_resolution_error(exc: BaseException) -> bool:
    text = repr(exc).lower()
    return any(
        marker in text
        for marker in (
            "nameresolutionerror",
            "failed to resolve",
            "name or service not known",
            "getaddrinfo failed",
            "nodename nor servname",
            "temporary failure in name resolution",
        )
    )


def _stringify_value(value: Any) -> str:
    if value is None:
        return ""
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (list, tuple, set)):
        return ",".join(_stringify_value(item) for item in value)
    return str(value)


def _stringify_mapping(value: Mapping[str, Any] | None) -> dict[str, str]:
    return {str(key): _stringify_value(item) for key, item in (value or {}).items()}


def _dfp_non_empty_items(params: Mapping[str, str]) -> list[tuple[str, str]]:
    return [(key, value) for key, value in sorted(params.items()) if value]


def _dfp_request_id() -> str:
    return uuid.uuid4().hex[:16]


@dataclass
class KwaiDeviceContext:
    did: str = field(default_factory=lambda: uuid.uuid4().hex)
    egid: str = ""
    rdid: str = ""
    ud: str = "0"
    cdid_tag: str = ""
    did_gt: str = ""
    odid: str = ""
    decision_did: str = ""
    decision: str = ""
    decision_inputs: str = ""
    oc: str = "UNKNOWN"
    new_oc: str = "ANTMAN_PC"
    channel: str = "ANTMAN_PC"
    sys: str = "ANDROID_12"
    mod: str = "Xiaomi(M2012K11AC)"
    net: str = "WIFI"
    language: str = "zh-cn"
    country_code: str = "CN"
    lat: str = "0"
    lon: str = "0"
    appver: str = "10.3.39.6655"
    version_short: str = "10.3"
    max_memory: str = "256"
    abi: str = "arm64"
    iuid: str = ""
    pm_tag: str = ""
    token: str = ""
    api_st: str = ""
    client_salt: str = ""
    service_token: str = ""
    azeroth_security: str = ""
    account_private_key_pem: str = ""
    cookie_extras: dict[str, str] = field(default_factory=dict)

    @classmethod
    def from_json_file(cls, path: str | Path) -> "KwaiDeviceContext":
        data = json.loads(Path(path).read_text(encoding="utf-8"))
        if not isinstance(data, Mapping):
            raise ValueError("device state JSON must be an object")
        return cls.from_mapping(data)

    @classmethod
    def from_mapping(cls, data: Mapping[str, Any]) -> "KwaiDeviceContext":
        device = cls()
        device.update_from_mapping(data, overwrite=True)
        return device

    def to_dict(self) -> dict[str, Any]:
        return {item.name: getattr(self, item.name) for item in fields(self)}

    def save_json_file(self, path: str | Path) -> None:
        Path(path).write_text(json.dumps(self.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")

    def to_redacted_dict(self) -> dict[str, Any]:
        return redact_state_mapping(self.to_dict())

    def import_login_capture(self, capture: Any, *, overwrite: bool = True) -> dict[str, str]:
        return import_login_capture_into_device(self, capture, overwrite=overwrite)

    def update_from_mapping(
        self,
        data: Mapping[str, Any],
        *,
        overwrite: bool = True,
        aliases: Mapping[str, tuple[str, ...]] | None = None,
        recursive: bool = True,
    ) -> dict[str, str]:
        changed: dict[str, str] = {}
        alias_map = aliases or DEVICE_FIELD_ALIASES
        known_fields = {item.name for item in fields(self)}
        mappings = _iter_mappings(data) if recursive else (data,)
        for mapping in mappings:
            for field_name in known_fields:
                if field_name not in mapping:
                    continue
                value = mapping.get(field_name)
                if value is None:
                    continue
                current = getattr(self, field_name)
                if isinstance(current, dict):
                    if not isinstance(value, Mapping):
                        continue
                    normalized = {str(key): str(item) for key, item in value.items() if item}
                    if overwrite or not current:
                        if current != normalized:
                            setattr(self, field_name, normalized)
                            changed[field_name] = json.dumps(normalized, ensure_ascii=False)
                    continue
                text = str(value)
                if not text:
                    continue
                if overwrite or not current:
                    if current != text:
                        setattr(self, field_name, text)
                        changed[field_name] = text
            for field_name, field_aliases in alias_map.items():
                if field_name not in known_fields:
                    continue
                for alias in field_aliases:
                    if alias not in mapping:
                        continue
                    value = mapping.get(alias)
                    if value is None:
                        continue
                    text = str(value)
                    if not text:
                        continue
                    current = getattr(self, field_name)
                    if overwrite or not current:
                        if current != text:
                            setattr(self, field_name, text)
                            changed[field_name] = text
                    break
        return changed


@dataclass
class SignedRequest:
    method: str
    url: str
    headers: dict[str, str]
    params: dict[str, str]
    data: bytes | dict[str, str] | None = None


@dataclass(frozen=True)
class RestEndpoint:
    name: str
    method: str
    path: str
    include_auth: bool = True
    safe: bool = False
    defaults: Mapping[str, str] = field(default_factory=dict)
    query_defaults: Mapping[str, str] = field(default_factory=dict)
    description: str = ""
    service: str = ""
    java_method: str = ""
    fields: tuple[str, ...] = ()
    query_fields: tuple[str, ...] = ()
    param_profile: str = "antman"
    rewrite_path: bool = True


REST_ENDPOINTS: dict[str, RestEndpoint] = {
    "startup": RestEndpoint(
        "startup",
        "POST",
        "/rest/system/startup",
        safe=True,
        defaults={"extId": "", "gp_referer": "", "oaid": ""},
        description="启动配置;extId 走 query,gp_referer/oaid 走 body",
    ),
    "version_check": RestEndpoint("version_check", "GET", "/rest/antman/versionCheck", safe=True),
    "system_speed": RestEndpoint("system_speed", "GET", "/rest/n/system/speed", safe=True),
    "system_dialog": RestEndpoint("system_dialog", "POST", "n/system/dialog", safe=False, defaults={"source": "startup"}),
    "cold_start_deeplink": RestEndpoint(
        "cold_start_deeplink",
        "POST",
        "/rest/n/system/dialog",
        safe=False,
        defaults={"source": "startup", "imeis": "", "oaid": "", "clipboard": ""},
    ),
    "dialog_report": RestEndpoint("dialog_report", "POST", "/rest/system/dialog/report", defaults={"source": "startup"}),
    "user_settings": RestEndpoint("user_settings", "POST", "n/user/settings"),
    "user_profile": RestEndpoint("user_profile", "POST", "n/user/profile"),
    "profile_list_friends": RestEndpoint("profile_list_friends", "POST", "n/user/profile/listFriends", defaults={"user": "", "cursor": "", "count": "20"}),
    "follow_recommend": RestEndpoint("follow_recommend", "POST", "n/user/recommend/follow"),
    "follow_recommend_delete": RestEndpoint("follow_recommend_delete", "POST", "n/user/recommend/follow/delete", defaults={"user_id": ""}),
    "mobile_check": RestEndpoint("mobile_check", "POST", "n/user/mobile/checker", include_auth=False, defaults={"mobileCountryCode": "86", "mobile": ""}),
    "request_mobile_code": RestEndpoint("request_mobile_code", "POST", "n/user/requestMobileCode", include_auth=False, defaults={"mobileCountryCode": "86", "mobile": "", "type": "27"}),
    "verify_code_login": RestEndpoint("verify_code_login", "POST", "n/user/login/mobileVerifyCode", include_auth=False, defaults={"code": "", "mobileCountryCode": "86", "mobile": "", "type": "27"}),
    "token_login": RestEndpoint("token_login", "POST", "n/user/login/token", include_auth=False, defaults={"token": "", "type": "302"}),
    "register_phone_v2": RestEndpoint("register_phone_v2", "POST", "n/user/register/mobileV2", include_auth=False, defaults={"userName": "", "mobileCountryCode": "86", "mobile": "", "mobileCode": "", "password": "", "gender": "U"}),
    "refresh_token": RestEndpoint("refresh_token", "POST", "n/token/infra/refreshToken", defaults={"token": "", "client_salt": ""}),
    "logout": RestEndpoint("logout", "POST", "n/user/logout", defaults={"token": "", "client_salt": ""}),
    "third_platform_login": RestEndpoint("third_platform_login", "POST", "user/thirdPlatformLogin", include_auth=False),
    "bind_platform": RestEndpoint("bind_platform", "POST", "n/user/thirdPlatform/bind", defaults={"platform": "", "accessToken": "", "openId": ""}),
    "verify_trust_device": RestEndpoint("verify_trust_device", "POST", "n/user/verifyTrustDevice", include_auth=False, defaults={"isAddAccount": "false"}),
    "reset_mobile": RestEndpoint("reset_mobile", "POST", "n/user/reset/mobile", include_auth=False),
    "reset_password": RestEndpoint("reset_password", "POST", "n/user/password/reset", include_auth=False),
    "contacts": RestEndpoint("contacts", "POST", "n/user/contacts", include_auth=False, defaults={"contacts": "", "page": ""}),
    "qq_friends": RestEndpoint("qq_friends", "POST", "n/user/qqFriends", include_auth=False, defaults={"data": "", "page": "0"}),
    "change_user_info": RestEndpoint("change_user_info", "POST", "n/user/modify", defaults={"user_name": "", "user_sex": "", "forceUnique": "false"}),
    "change_birthday": RestEndpoint("change_birthday", "POST", "n/user/modify", defaults={"birthdayTs": ""}),
    "change_city_code": RestEndpoint("change_city_code", "POST", "n/user/modify", defaults={"cityCode": ""}),
    "change_user_sex": RestEndpoint("change_user_sex", "POST", "n/user/modify", defaults={"user_sex": ""}),
    "change_option": RestEndpoint("change_option", "POST", "n/user/changeOption", defaults={"key": "", "value": ""}),
    "change_setting": RestEndpoint("change_setting", "POST", "n/user/changeSetting", defaults={"key": "", "value": ""}),
    "modify_profile_background": RestEndpoint("modify_profile_background", "POST", "n/user/modifyProfileBG"),
    "delete_profile_background": RestEndpoint("delete_profile_background", "POST", "n/user/modifyProfileBG", defaults={"delete": "true"}),
    "antman_video_feed_hot": RestEndpoint(
        "antman_video_feed_hot",
        "GET",
        "/rest/n/feed/hot",
        safe=False,
        defaults={"count": "1", "pcursor": ""},
        description="Antman-route feed probe retained for comparison; CTF video feed uses video_feed_hot",
    ),
    "antman_video_photo_comments": RestEndpoint(
        "antman_video_photo_comments",
        "GET",
        "/rest/photo/comment/list",
        safe=False,
        defaults={"photoId": "", "pcursor": "", "count": "20"},
        description="Antman-route comment probe retained for comparison; CTF comments use video_photo_comments",
    ),
    "video_feed_hot": RestEndpoint(
        "video_feed_hot",
        "POST",
        "/rest/n/feed/hot",
        safe=False,
        defaults={"count": "1", "pcursor": ""},
        description="read-only main-app hot video feed; verified result=1 with main_video params",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "video_feed_profile": RestEndpoint(
        "video_feed_profile",
        "POST",
        "/rest/n/feed/profile2",
        safe=False,
        defaults={"count": "1", "pcursor": "", "user_id": ""},
        description="read-only main-app profile feed v2; requires a user_id from feed",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "video_photo_info": RestEndpoint(
        "video_photo_info",
        "POST",
        "/rest/n/photo/info",
        safe=False,
        defaults={"photoIds": ""},
        description="read-only main-app photo info; pass photoId=... or photoIds=... from feed",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "video_photo_comments": RestEndpoint(
        "video_photo_comments",
        "POST",
        "/rest/n/comment/list/v2",
        safe=False,
        defaults={"photoId": "", "count": "20", "order": "desc", "enableEmotion": "true"},
        description="read-only main-app comment list v2; verified result=1 with a photoId from feed",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "azeroth_configs": RestEndpoint(
        "azeroth_configs",
        "GET",
        "/rest/zt/appsupport/configs",
        safe=False,
        defaults={},
        description="AzerothService.a @QueryMap; uses AzerothParamProcessor rather than KwaiParams",
        service="AzerothService",
        java_method="a",
    ),
    "plugin_startup": RestEndpoint(
        "plugin_startup",
        "POST",
        "system/startup",
        safe=False,
        defaults={"extId": "", "gp_referer": "", "oaid": ""},
        description="PluginLoadKSwitchManager.b.startup; extId is @Query, gp_referer/oaid are @Field",
        service="PluginLoadKSwitchManager.b",
        java_method="startup",
        fields=("gp_referer", "oaid"),
    ),
    "test_speed_dynamic_url": RestEndpoint(
        "test_speed_dynamic_url",
        "POST",
        "@Url",
        safe=False,
        defaults={"url": "", "op": ""},
        description="TestSpeedService.a; url is Retrofit @Url, op is @Field",
        service="TestSpeedService",
        java_method="a",
        fields=("url", "op"),
    ),
}

_MANUAL_REST_ENDPOINT_NAMES = frozenset(REST_ENDPOINTS)


def _snake_name(value: str) -> str:
    value = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", value)
    value = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value)
    return re.sub(r"[^a-zA-Z0-9]+", "_", value).strip("_").lower()


def _normalize_service_name(service: str) -> str:
    service = service.strip().strip(";")
    if "/" in service:
        service = service.rsplit("/", 1)[-1]
    if service.startswith("L"):
        service = service[1:]
    return service


def _endpoint_needs_login_auth(service: str, java_method: str, path: str) -> bool:
    lowered = f"{service} {java_method} {path}".lower()
    anonymous_markers = (
        "login",
        "register",
        "requestmobilecode",
        "resetpassword",
        "resetmobile",
        "thirdplatformlogin",
        "mobile/checker",
    )
    return not any(marker in lowered for marker in anonymous_markers)


def _parse_retrofit_annotation_file(path: Path) -> dict[str, RestEndpoint]:
    if not path.exists():
        return {}
    endpoints: dict[str, RestEndpoint] = {}
    current: dict[str, Any] | None = None

    def flush() -> None:
        nonlocal current
        if not current or not current.get("http_method") or not current.get("path"):
            current = None
            return
        service = _normalize_service_name(str(current["service"]))
        java_method = str(current["java_method"])
        prefix = {"KwaiApiService": "api", "KwaiHttpsService": "https", "KwaiPayService": "pay"}.get(service, _snake_name(service))
        base_name = f"{prefix}_{_snake_name(java_method)}"
        name = base_name
        suffix = 2
        while name in _MANUAL_REST_ENDPOINT_NAMES or name in endpoints:
            name = f"{base_name}_{suffix}"
            suffix += 1
        fields_tuple = tuple(str(field) for field in current.get("fields", ()))
        endpoints[name] = RestEndpoint(
            name=name,
            method=str(current["http_method"]),
            path=str(current["path"]),
            include_auth=_endpoint_needs_login_auth(service, java_method, str(current["path"])),
            safe=False,
            defaults={field: "" for field in fields_tuple},
            description="auto parsed from Retrofit annotations",
            service=service,
            java_method=java_method,
            fields=fields_tuple,
        )
        current = None

    service_line = re.compile(r"^(L[^;]+;)\s+(\S+)\s+(\S+)\s+PARAMS\s+(.*)")
    http_line = re.compile(r"(GET|POST)\{'value': '([^']+)'\}")
    field_line = re.compile(r"'value': '([^']+)'")
    for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        line = raw_line.strip()
        match = service_line.match(line)
        if match:
            flush()
            current = {"service": match.group(1), "java_method": match.group(2), "http_method": "", "path": "", "fields": []}
            continue
        if current is None:
            continue
        match = http_line.search(line)
        if match:
            current["http_method"] = match.group(1)
            current["path"] = match.group(2).strip()
            continue
        if "Lretrofit2/http/" in line:
            match = field_line.search(line)
            if match:
                current["fields"].append(match.group(1))
    flush()
    return endpoints


def _load_retrofit_annotation_json(path: Path) -> dict[str, RestEndpoint]:
    if not path.exists():
        return {}
    try:
        records = json.loads(path.read_text(encoding="utf-8-sig", errors="replace"))
    except json.JSONDecodeError:
        return {}
    if not isinstance(records, list):
        return {}
    endpoints: dict[str, RestEndpoint] = {}
    for record in records:
        if not isinstance(record, Mapping):
            continue
        http_method = str(record.get("http_method") or "").upper()
        endpoint_path = str(record.get("path") or "").strip()
        java_method = str(record.get("method_name") or record.get("java_method") or "")
        if not http_method or not endpoint_path or not java_method:
            continue
        service = _normalize_service_name(str(record.get("service") or record.get("class_name") or ""))
        prefix = {"KwaiApiService": "api", "KwaiHttpsService": "https", "KwaiPayService": "pay"}.get(
            service, _snake_name(service)
        )
        base_name = f"{prefix}_{_snake_name(java_method)}" if prefix else _snake_name(java_method)
        name = base_name
        suffix = 2
        while name in _MANUAL_REST_ENDPOINT_NAMES or name in endpoints:
            name = f"{base_name}_{suffix}"
            suffix += 1
        body_fields = tuple(str(field) for field in (record.get("body_fields") or record.get("fields") or ()) if field)
        query_fields = tuple(str(field) for field in (record.get("query_fields") or ()) if field)
        endpoints[name] = RestEndpoint(
            name=name,
            method=http_method,
            path=endpoint_path,
            include_auth=_endpoint_needs_login_auth(service, java_method, endpoint_path),
            safe=False,
            defaults={field: "" for field in body_fields},
            query_defaults={field: "" for field in query_fields},
            description="auto parsed from full Retrofit annotation JSON",
            service=service,
            java_method=java_method,
            fields=body_fields,
            query_fields=query_fields,
        )
    return endpoints


def load_recovered_rest_endpoints(path: str | Path | None = None) -> dict[str, RestEndpoint]:
    if path is None:
        root = Path(__file__).resolve().parents[1]
        full_json = root / "tmp_rest_annotations_full.json"
        if full_json.exists():
            return _load_retrofit_annotation_json(full_json)
        path = root / "tmp_plugin_annotations.txt"
    path = Path(path)
    if path.suffix.lower() == ".json":
        return _load_retrofit_annotation_json(path)
    return _parse_retrofit_annotation_file(path)


REST_ENDPOINTS.update(load_recovered_rest_endpoints())

HAR_OBSERVED_REST_ENDPOINTS = {
    "api_promotion_widget": RestEndpoint(
        "api_promotion_widget",
        "GET",
        "/rest/antman/promotion/widget",
        safe=True,
        description="observed in HAR: promotion/widget information",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "api_encode_android": RestEndpoint(
        "api_encode_android",
        "POST",
        "/rest/antman/encode/android",
        safe=True,
        description="observed in HAR: encode/android configuration probe",
        param_profile="main_video",
        rewrite_path=False,
    ),
    "api_system_stat_observed": RestEndpoint(
        "api_system_stat_observed",
        "POST",
        "/rest/antman/system/stat",
        safe=False,
        defaults={
            "mark": "",
            "manufacturer": "",
            "startup": "4",
            "channel": "ANTMAN_PC",
            "original_channel": "UNKNOWN",
            "data": "",
            "third_platform_tokens": "[]",
            "enable_push": "1",
        },
        description="observed in HAR: telemetry/stat; not marked safe",
        param_profile="main_video",
        rewrite_path=False,
    ),
}
for _name, _endpoint in HAR_OBSERVED_REST_ENDPOINTS.items():
    REST_ENDPOINTS.setdefault(_name, _endpoint)


def _retune_rest_endpoint(name: str, **edits: Any) -> None:
    endpoint = REST_ENDPOINTS.get(name)
    if endpoint is not None:
        REST_ENDPOINTS[name] = replace(endpoint, **edits)


for _name in (
    "mobile_check",
    "request_mobile_code",
    "verify_code_login",
    "token_login",
    "register_phone_v2",
    "verify_trust_device",
):
    _retune_rest_endpoint(_name, param_profile="main_video_login", rewrite_path=False)

for _name in (
    "user_settings",
    "user_profile",
    "profile_list_friends",
    "api_fetch_hybrid_version",
    "api_check_hybrid_update",
    "api_check_url_update",
    "api_experiment",
    "api_lab_config_response",
    "api_encode_config",
    "api_app_info",
):
    _retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=False)

for _name in ("api_startup", "plugin_startup", "api_experiment"):
    _retune_rest_endpoint(_name, safe=True)

_retune_rest_endpoint("azeroth_configs", param_profile="azeroth", rewrite_path=True)

DEFAULT_REST_SMOKE_ENDPOINT_NAMES = (
    "api_experiment",
    "api_startup",
    "plugin_startup",
    "startup",
    "system_speed",
    "version_check",
)

SEARCH_REST_ENDPOINT_NAMES = frozenset(
    {
        "api_search",
        "api_search_home",
        "api_search_kwai_hot_billboard",
        "api_search_recommend",
        "api_search_suggest",
        "api_search_tag",
        "api_search_tag_recommend",
        "api_search_user",
    }
)

VIDEO_REST_ENDPOINT_NAMES = frozenset(
    {
        "api_comment_list_by_pivot",
        "api_comment_list_v2",
        "api_comment_sub_list",
        "api_feed_like_list",
        "api_feed_my_follow",
        "api_feed_my_follow_live_stream",
        "api_feed_my_follow_photo",
        "api_feed_near_by",
        "api_feed_tag",
        "api_get_domino_feeds",
        "api_get_feed_selection",
        "api_get_hot_items",
        "api_get_hot_photos_in_magic_face_tag",
        "api_get_hot_photos_in_music_tag",
        "api_get_hot_photos_in_same_frame_tag",
        "api_get_hot_photos_in_text_tag",
        "api_get_latest_photos_in_magic_tag",
        "api_get_latest_photos_in_music_tag",
        "api_get_latest_photos_in_same_frame_tag",
        "api_get_latest_photos_in_text_tag",
        "api_get_magic_face_tag_info",
        "api_get_music_tag_info",
        "api_get_hot_items",
        "api_get_photo_infos",
        "api_likers",
        "api_music",
        "api_profile_feed",
        "api_get_same_frame_tag_info",
        "api_get_text_tag_info",
        "api_tag_detail",
        "video_feed_hot",
        "video_feed_profile",
        "video_photo_comments",
        "video_photo_info",
    }
)

USER_REST_ENDPOINT_NAMES = frozenset(
    {
        "api_device_verify_status",
        "api_get_all_alias_list",
        "api_get_all_follow_users",
        "api_get_at_users",
        "api_get_follow_users",
        "api_get_friend_users",
        "api_get_relation_friends",
        "api_get_users_profile_batch",
        "api_profile_feed",
        "api_profile_list_friends",
        "api_trust_device_list",
        "api_user_info",
        "api_user_profile_v2",
        "profile_list_friends",
        "user_profile",
        "user_settings",
    }
)

HAR_OBSERVED_REST_ENDPOINT_NAMES = frozenset(
    {
        "api_ad_list",
        "api_check_photo",
        "api_encode_android",
        "api_get_follow_users",
        "api_live_auth_status",
        "api_promotion_widget",
        "api_update_config",
        "system_dialog",
    }
)

MESSAGE_REST_ENDPOINT_NAMES = frozenset(
    {
        "api_get_friend_users",
        "api_get_share_user_list",
        "api_get_users_profile_batch",
        "api_message_dialog",
        "api_message_load",
        "api_nebula_notify_load",
        "api_news_load",
        "api_notify_load",
    }
)

REST_INFO_CATEGORIES = {
    "message": MESSAGE_REST_ENDPOINT_NAMES,
    "observed": HAR_OBSERVED_REST_ENDPOINT_NAMES,
    "search": SEARCH_REST_ENDPOINT_NAMES,
    "user": USER_REST_ENDPOINT_NAMES,
    "video": VIDEO_REST_ENDPOINT_NAMES,
}

INFO_REST_ENDPOINT_NAMES = frozenset().union(*REST_INFO_CATEGORIES.values())

for _name in INFO_REST_ENDPOINT_NAMES:
    _retune_rest_endpoint(_name, safe=True)

for _name in INFO_REST_ENDPOINT_NAMES - {"profile_list_friends", "user_profile", "user_settings"}:
    if _name.startswith("video_"):
        _retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=False)
    else:
        _retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=True)

_retune_rest_endpoint("api_search_tag_recommend", param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_send_message", param_profile="main_video", rewrite_path=True)
for _name in HAR_OBSERVED_REST_ENDPOINT_NAMES:
    _retune_rest_endpoint(_name, safe=True, param_profile="main_video", rewrite_path=True)
_retune_rest_endpoint("api_promotion_widget", safe=True, param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_encode_android", safe=True, param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_system_stat_observed", safe=False, param_profile="main_video", rewrite_path=False)


class KwaiClient:
    def __init__(
        self,
        device: KwaiDeviceContext | None = None,
        *,
        host: str = "https://apissl.ksapisrv.com",
        appkey: str = APPKEY_MAIN,
        assets_root: str = "resources/assets",
        zt_table: KwaiZtTable | None = None,
        timeout: float = 15.0,
        session: Any = None,
    ) -> None:
        self.device = device or KwaiDeviceContext()
        self.host = host.rstrip("/")
        self.appkey = appkey
        self.assets_root = str(assets_root)
        self.timeout = timeout
        self.sig3_mode = "10418"
        try:
            self.zt_table = zt_table or find_zt_table(appkey, assets_root)
        except Exception:
            self.zt_table = zt_table
        try:
            self.azeroth_zt_table = find_zt_table(APPKEY_AZEROTH, assets_root)
        except Exception:
            self.azeroth_zt_table = None
        if session is not None:
            self.session = session
        else:
            try:
                import requests

                self.session = requests.Session()
            except Exception:
                self.session = None

    def import_login_capture(self, capture: Any, *, overwrite: bool = True) -> dict[str, str]:
        return self.device.import_login_capture(capture, overwrite=overwrite)

    def base_headers(self) -> dict[str, str]:
        headers = {
            "User-Agent": "kwai-android",
            "Accept-Language": self.device.language,
            "X-REQUESTID": str(int(time.monotonic() * 1000)),
            "Connection": "keep-alive",
        }
        cookie = self.cookie_header()
        if cookie:
            headers["Cookie"] = cookie
        return headers

    def cookie_header(self) -> str:
        cookies = dict(self.device.cookie_extras)
        if self.device.token:
            cookies.setdefault("token", self.device.token)
        return ";".join(f"{key}={value}" for key, value in cookies.items() if value)

    def common_params(self) -> dict[str, str]:
        params = {
            "lat": self.device.lat,
            "lon": self.device.lon,
            "ver": self.device.version_short,
            "ud": self.device.ud,
            "sys": self.device.sys,
            "c": self.device.channel,
            "oc": self.device.oc,
            "newOc": self.device.new_oc,
            "net": self.device.net,
            "did": self.device.did,
            "did_tag": "1",
            "cdid_tag": self.device.cdid_tag,
            "rdid": self.device.rdid,
            "egid": self.device.egid,
            "mod": self.device.mod,
            "app": "0" if self.device.channel.upper() != "GOOGLE_PLAY" else "1",
            "language": self.device.language,
            "country_code": self.device.country_code,
            "appver": self.device.appver,
            "ftt": "",
            "iuid": self.device.iuid,
            "max_memory": self.device.max_memory,
            "kpn": "KUAISHOU_ANTMAN",
            "apptype": "31",
            "kpf": "ANDROID_PHONE",
            "browseType": "1",
            "isAntman": "true",
            "abi": self.device.abi,
        }
        if self.device.did_gt:
            params["did_gt"] = self.device.did_gt
        if self.device.pm_tag:
            params["pm_tag"] = self.device.pm_tag
        return params

    def main_video_params(self, *, use_device_version: bool = False, login_compat_version: bool = False) -> dict[str, str]:
        did = self.device.did
        if not did.startswith("ANDROID_"):
            did = "ANDROID_" + re.sub(r"[^0-9a-fA-F]", "", did)[:16].ljust(16, "0")
        did_gt = self.device.did_gt or "1562212339132"
        channel = "MYAPP,1"
        if use_device_version:
            appver = self.device.appver
            version_short = self.device.version_short
            max_memory = self.device.max_memory
        elif login_compat_version:
            appver = MAIN_VIDEO_LOGIN_APPVER
            version_short = MAIN_VIDEO_LOGIN_VER
            max_memory = MAIN_VIDEO_MAX_MEMORY
        else:
            appver = MAIN_VIDEO_LEGACY_APPVER
            version_short = MAIN_VIDEO_LEGACY_VER
            max_memory = MAIN_VIDEO_MAX_MEMORY
        return {
            "app": "0",
            "lon": self.device.lon,
            "did_gt": did_gt,
            "c": channel,
            "sys": self.device.sys if self.device.sys and self.device.sys != "ANDROID_12" else "ANDROID_8.1",
            "isp": "",
            "mod": self.device.mod if self.device.mod else "LGE(AOSP on TTOG)",
            "did": did,
            "hotfix_ver": "",
            "ver": version_short,
            "net": self.device.net,
            "country_code": self.device.country_code.upper() if self.device.country_code else "CN",
            "iuid": self.device.iuid,
            "appver": appver,
            "max_memory": max_memory,
            "oc": channel,
            "ftt": "",
            "kpn": "KUAISHOU",
            "ud": self.device.ud,
            "language": self.device.language,
            "kpf": "ANDROID_PHONE",
            "lat": self.device.lat,
        }

    def azeroth_did(self) -> str:
        did = self.device.did
        if did.startswith("ANDROID_"):
            return did
        cleaned = re.sub(r"[^0-9a-fA-F]", "", did)[:16].ljust(16, "0")
        return "ANDROID_" + cleaned

    def azeroth_params(self, *, use_latest_version: bool = True) -> dict[str, str]:
        appver = AZEROTH_LATEST_APPVER if use_latest_version else self.device.appver
        ver = AZEROTH_LATEST_VER if use_latest_version else self.device.version_short
        channel = self.device.oc or "UNKNOWN"
        params = {
            "kpn": "KUAISHOU",
            "kpf": "ANDROID_PHONE",
            "appver": appver,
            "ver": ver,
            "gid": "",
            "did": self.azeroth_did(),
            "userId": self.device.ud or "0",
            "lat": str(self.device.lat if self.device.lat else "0.0"),
            "lon": str(self.device.lon if self.device.lon else "0.0"),
            "mod": self.device.mod or "Xiaomi(M2012K11AC)",
            "net": self.device.net or "WIFI",
            "sys": self.device.sys or "ANDROID_12",
            "os": "android",
            "c": channel,
            "language": self.device.language or "zh-cn",
            "countryCode": self.device.country_code.upper() if self.device.country_code else "CN",
            "mcc": "",
            "androidApiLevel": AZEROTH_ANDROID_API_LEVEL,
        }
        if self.device.service_token:
            params["kuaishou.api_st"] = self.device.service_token
        elif self.device.api_st:
            params["kuaishou.api_st"] = self.device.api_st
        if self.device.token:
            params["token"] = self.device.token
        return params

    def params_for_profile(self, profile: str) -> dict[str, str]:
        if profile == "auto":
            profile = "antman"
        if profile in {"azeroth", "azeroth_latest"}:
            return self.azeroth_params(use_latest_version=True)
        if profile == "azeroth_device":
            return self.azeroth_params(use_latest_version=False)
        if profile == "main_video":
            return self.main_video_params()
        if profile in {"main_video_login", "main_video_compat"}:
            return self.main_video_params(login_compat_version=True)
        if profile == "main_video_device":
            return self.main_video_params(use_device_version=True)
        if profile != "antman":
            raise ValueError(f"unsupported parameter profile: {profile}")
        return self.common_params()

    @staticmethod
    def auto_param_profile(path_or_url: str) -> str:
        path = urllib.parse.urlsplit(path_or_url).path if "://" in path_or_url else path_or_url
        if not path.startswith("/"):
            path = "/" + path
        login_fragments = (
            "/user/mobile/checker",
            "/user/requestMobileCode",
            "/user/login/mobileVerifyCode",
            "/user/register/mobileV2",
            "/user/login/token",
            "/user/thirdPlatformLogin",
            "/wechat/oauth2/authByCode",
            "/user/verifyTrustDevice",
        )
        if any(fragment in path for fragment in login_fragments):
            return "main_video_login"
        video_fragments = (
            "/feed/",
            "/photo/",
            "/comment/",
            "/search/",
        )
        if any(fragment in path for fragment in video_fragments):
            return "main_video"
        return "antman"

    def auth_params(self, *, include_client_salt: bool = False) -> dict[str, str]:
        params = {"os": "android", "client_key": "3c2cd3f3"}
        if self.device.token:
            params["token"] = self.device.token
        if self.device.api_st:
            params["kuaishou.api_st"] = self.device.api_st
        if include_client_salt and self.device.client_salt:
            params["client_salt"] = self.device.client_salt
        return params

    @staticmethod
    def rewrite_path(path: str) -> str:
        if "token/infra/refreshToken" in path:
            return path
        if not path.startswith("/"):
            path = "/" + path
        if path.startswith("/rest/antman/"):
            return path
        if path.startswith("/rest/n/"):
            return "/rest/antman/" + path[len("/rest/n/") :]
        if path.startswith("/rest/"):
            return "/rest/antman/" + path[len("/rest/") :]
        return path

    def build_url(self, path_or_url: str, *, rewrite_path: bool = True) -> tuple[str, str]:
        parsed = urllib.parse.urlsplit(path_or_url)
        if parsed.scheme and parsed.netloc:
            path = self.rewrite_path(parsed.path) if rewrite_path else parsed.path
            if not path.startswith("/"):
                path = "/" + path
            url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, parsed.query, ""))
            return url, path
        path = self.rewrite_path(path_or_url) if rewrite_path else path_or_url
        if not path.startswith("/"):
            path = "/" + path
        return self.host + path, path

    def sign_query(
        self,
        path: str,
        user_params: Mapping[str, Any] | None = None,
        *,
        include_auth: bool = True,
        now: int | None = None,
        counter: int = 1,
        sig3_mode: str | None = None,
        param_profile: str = "antman",
    ) -> dict[str, str]:
        user = {str(k): "" if v is None else str(v) for k, v in (user_params or {}).items()}
        request_salt = user.pop("client_salt", None) or self.device.client_salt
        params = self.params_for_profile(param_profile)
        if include_auth and param_profile == "antman":
            params.update(self.auth_params())
        params.update(user)
        sig = kwai_sig(params)
        params["sig"] = sig
        if request_salt:
            params["__NStokensig"] = kwai_token_sig(sig, request_salt)
        ns_input = (path + sig).encode("utf-8")
        params["__NS_sig3"] = self.make_sig3(ns_input, now=now, counter=counter, mode=sig3_mode)
        return params

    def sign_form(
        self,
        path: str,
        form_params: Mapping[str, Any] | None = None,
        *,
        query_params: Mapping[str, Any] | None = None,
        body_bytes: bytes | None = None,
        include_auth: bool = True,
        now: int | None = None,
        counter: int = 1,
        sig3_mode: str | None = None,
    ) -> dict[str, str]:
        _, body = self.sign_form_parts(
            path,
            form_params,
            query_params=query_params,
            body_bytes=body_bytes,
            include_auth=include_auth,
            now=now,
            counter=counter,
            sig3_mode=sig3_mode,
        )
        return body

    def sign_form_parts(
        self,
        path: str,
        form_params: Mapping[str, Any] | None = None,
        *,
        query_params: Mapping[str, Any] | None = None,
        body_bytes: bytes | None = None,
        include_auth: bool = True,
        now: int | None = None,
        counter: int = 1,
        sig3_mode: str | None = None,
        param_profile: str = "antman",
    ) -> tuple[dict[str, str], dict[str, str]]:
        form = {str(k): "" if v is None else str(v) for k, v in (form_params or {}).items()}
        query = {str(k): "" if v is None else str(v) for k, v in (query_params or {}).items()}
        query_signed = self.params_for_profile(param_profile)
        query_signed.update(query)
        auth = self.auth_params(include_client_salt=True)
        request_salt = form.pop("client_salt", None) or query_signed.pop("client_salt", None) or auth.pop("client_salt", None)
        body_signed = {"os": auth.pop("os", "android"), "client_key": auth.pop("client_key", "3c2cd3f3")}
        if include_auth and param_profile == "antman":
            body_signed.update(auth)
        body_signed.update(form)
        sig = kwai_sig(query_signed, body_signed)
        body_signed["sig"] = sig
        if request_salt:
            body_signed["__NStokensig"] = kwai_token_sig(sig, request_salt)
        body_signed["__NS_sig3"] = self.make_sig3((path + sig).encode("utf-8"), now=now, counter=counter, mode=sig3_mode)
        return query_signed, body_signed

    def sign_binary_query(
        self,
        body_bytes: bytes,
        extra_params: Mapping[str, Any] | None = None,
        *,
        include_auth: bool = True,
    ) -> dict[str, str]:
        params = self.common_params()
        params["os"] = "android"
        params["client_key"] = "3c2cd3f3"
        params.update({str(k): "" if v is None else str(v) for k, v in (extra_params or {}).items()})
        body_md5 = hashlib.md5(body_bytes).hexdigest()
        sign_map = dict(params)
        sign_map["bodyMd5"] = body_md5
        if include_auth:
            auth_for_sign = self.auth_params(include_client_salt=True)
            request_salt = auth_for_sign.pop("client_salt", None)
            for key in ("token", "kuaishou.api_st"):
                if key in auth_for_sign:
                    sign_map[key] = auth_for_sign[key]
                    params[key] = auth_for_sign[key]
        else:
            request_salt = self.device.client_salt
        sig2 = kwai_sig(sign_map)
        params["sig2"] = sig2
        if request_salt:
            params["__NStokensig"] = kwai_token_sig(sig2, request_salt)
        return params

    def make_sig3(
        self,
        data: bytes | str,
        *,
        now: int | None = None,
        counter: int = 1,
        mode: str | None = None,
    ) -> str:
        mode = mode or self.sig3_mode
        if mode == "10418":
            if not self.zt_table:
                raise ValueError("10418 sig3 requires a zt table")
            return sig3_10418(data, self.zt_table)
        if mode == "10417":
            return sig3_10417(data, self.zt_table, now=now, counter=counter)
        if mode == "10405":
            return sig3_10405(data, table=self.zt_table, now=now)
        raise ValueError(f"unsupported sig3 mode: {mode}")

    def make_azeroth_sig3(
        self,
        data: bytes | str,
        *,
        now: int | None = None,
        counter: int = 1,
        mode: str | None = None,
    ) -> str:
        mode = mode or "10418"
        table = self.azeroth_zt_table or self.zt_table
        if mode == "10418":
            if not table:
                raise ValueError("Azeroth 10418 sig3 requires a zt table")
            return sig3_10418(data, table)
        if mode == "10417":
            return sig3_10417(data, table, now=now, counter=counter)
        if mode == "10405":
            return sig3_10405(data, table=table, now=now)
        raise ValueError(f"unsupported Azeroth sig3 mode: {mode}")

    def sign_azeroth_parts(
        self,
        method: str,
        path: str,
        form_params: Mapping[str, Any] | None = None,
        *,
        query_params: Mapping[str, Any] | None = None,
        include_auth: bool = True,
        now: int | None = None,
        counter: int = 1,
        sig3_mode: str | None = None,
        param_profile: str = "azeroth",
    ) -> tuple[dict[str, str], dict[str, str]]:
        query_signed = self.params_for_profile(param_profile)
        if not include_auth:
            query_signed.pop("kuaishou.api_st", None)
            query_signed.pop("token", None)
        query_signed.update(_stringify_mapping(query_params))
        body_signed = _stringify_mapping(form_params)
        sign_map = dict(query_signed)
        sign_map.update(body_signed)
        client_sign = azeroth_hmac_client_sign(method, path, sign_map, self.device.azeroth_security)
        if client_sign:
            body_signed["__clientSign"] = client_sign
        canonical = azeroth_canonical(method, path, sign_map)
        body_signed["__NS_sig3"] = self.make_azeroth_sig3(
            canonical.encode("utf-8"), now=now, counter=counter, mode=sig3_mode
        )
        return query_signed, body_signed

    def signed_azeroth_request(
        self,
        method: str,
        path_or_url: str,
        *,
        params: Mapping[str, Any] | None = None,
        data: Mapping[str, Any] | bytes | None = None,
        headers: Mapping[str, str] | None = None,
        sign_body: bool | None = None,
        include_auth: bool = True,
        sig3_mode: str | None = None,
        param_profile: str = "azeroth",
        rewrite_path: bool = False,
        post_sign_data: Mapping[str, Any] | None = None,
    ) -> SignedRequest:
        method = method.upper()
        url, path = self.build_url(path_or_url, rewrite_path=rewrite_path)
        merged_headers = self.base_headers()
        if headers:
            merged_headers.update(headers)
        if sign_body is None:
            sign_body = method in {"POST", "PUT", "PATCH"}
        if isinstance(data, bytes):
            raise ValueError("Azeroth signing currently supports form requests, not raw bytes")
        body_params = _stringify_mapping(data)
        if not sign_body:
            query_params = dict(body_params)
            query_params.update(_stringify_mapping(params))
            signed_params, signed_body = self.sign_azeroth_parts(
                method,
                path,
                {},
                query_params=query_params,
                include_auth=include_auth,
                sig3_mode=sig3_mode,
                param_profile=param_profile,
            )
            signed_params.update(signed_body)
            signed_params.update(_stringify_mapping(post_sign_data))
            return SignedRequest(method, url, merged_headers, signed_params, None)
        signed_params, signed_body = self.sign_azeroth_parts(
            method,
            path,
            body_params,
            query_params=params,
            include_auth=include_auth,
            sig3_mode=sig3_mode,
            param_profile=param_profile,
        )
        signed_body.update(_stringify_mapping(post_sign_data))
        merged_headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
        return SignedRequest(method, url, merged_headers, signed_params, signed_body)

    def signed_request(
        self,
        method: str,
        path_or_url: str,
        *,
        params: Mapping[str, Any] | None = None,
        data: Mapping[str, Any] | bytes | None = None,
        headers: Mapping[str, str] | None = None,
        sign_body: bool | None = None,
        include_auth: bool = True,
        sig3_mode: str | None = None,
        param_profile: str = "antman",
        rewrite_path: bool = True,
        post_sign_data: Mapping[str, Any] | None = None,
    ) -> SignedRequest:
        method = method.upper()
        if param_profile == "auto":
            param_profile = self.auto_param_profile(path_or_url)
            if param_profile != "antman":
                rewrite_path = False
        if param_profile.startswith("azeroth"):
            return self.signed_azeroth_request(
                method,
                path_or_url,
                params=params,
                data=data,
                headers=headers,
                sign_body=sign_body,
                include_auth=include_auth,
                sig3_mode=sig3_mode,
                param_profile=param_profile,
                rewrite_path=rewrite_path,
                post_sign_data=post_sign_data,
            )
        url, path = self.build_url(path_or_url, rewrite_path=rewrite_path)
        merged_headers = self.base_headers()
        if headers:
            merged_headers.update(headers)
        if sign_body is None:
            sign_body = method in {"POST", "PUT", "PATCH"}
        if sign_body:
            if isinstance(data, bytes):
                body_bytes = data
                signed_params = self.sign_binary_query(body_bytes, params, include_auth=include_auth)
                merged_headers.setdefault("Content-Type", "application/octet-stream")
                return SignedRequest(method, url, merged_headers, signed_params, data)
            body_params = {str(k): "" if v is None else str(v) for k, v in (data or {}).items()}
            signed_params, signed_body = self.sign_form_parts(
                path,
                body_params,
                query_params=params,
                include_auth=include_auth,
                sig3_mode=sig3_mode,
                param_profile=param_profile,
            )
            signed_body.update(_stringify_mapping(post_sign_data))
            merged_headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
            return SignedRequest(method, url, merged_headers, signed_params, signed_body)
        signed_params = self.sign_query(
            path,
            params,
            include_auth=include_auth,
            sig3_mode=sig3_mode,
            param_profile=param_profile,
        )
        signed_params.update(_stringify_mapping(post_sign_data))
        return SignedRequest(method, url, merged_headers, signed_params, None)

    def send(self, signed: SignedRequest, *, capture_dynamic: bool = True, **kwargs: Any) -> Any:
        if self.session is None:
            raise RuntimeError("requests is not installed; inspect SignedRequest or install requests")
        request_kwargs = {
            "headers": signed.headers,
            "params": signed.params,
            "timeout": self.timeout,
        }
        if isinstance(signed.data, bytes):
            request_kwargs["data"] = signed.data
        elif signed.data is not None:
            request_kwargs["data"] = signed.data
        request_kwargs.update(kwargs)
        try:
            response = self.session.request(signed.method, signed.url, **request_kwargs)
        except Exception as exc:
            hostname = urllib.parse.urlsplit(signed.url).hostname or ""
            fallback_ips = DNS_OVERRIDE_IPS.get(hostname, ())
            if not fallback_ips or not _is_dns_resolution_error(exc):
                raise
            last_error: BaseException = exc
            for ip_address in fallback_ips:
                try:
                    with _temporary_dns_override(hostname, ip_address):
                        response = self.session.request(signed.method, signed.url, **request_kwargs)
                    break
                except Exception as retry_exc:
                    last_error = retry_exc
            else:
                raise last_error
        if capture_dynamic:
            self.capture_dynamic_fields(response, request_path=urllib.parse.urlsplit(signed.url).path)
        return response

    def fetch_captcha_challenge(self, error_url: str) -> tuple[CaptchaChallenge, Any]:
        if self.session is None:
            raise RuntimeError("requests is not installed; cannot fetch captcha page")
        headers = {
            "User-Agent": "Mozilla/5.0 (Linux; Android 10; Kwai) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Referer": "https://app.m.kuaishou.com/",
        }
        response = self.session.get(error_url, headers=headers, timeout=self.timeout)
        return parse_captcha_challenge(error_url, response.text), response

    def captcha_verify(
        self,
        key: str,
        ticket: str,
        *,
        uri: str = "/rest/n/user/login/mobileVerifyCode",
        verify_type: int | str = 7,
        referer: str = "",
    ) -> Any:
        if self.session is None:
            raise RuntimeError("requests is not installed; cannot verify captcha ticket")
        try:
            request_type: int | str = int(str(verify_type))
        except ValueError:
            request_type = str(verify_type)
        headers = {
            "User-Agent": "Mozilla/5.0 (Linux; Android 10; Kwai) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36",
            "Accept": "application/json, text/plain, */*",
            "Content-Type": "application/json; charset=utf-8",
            "Origin": "https://app.m.kuaishou.com",
            "Referer": referer or "https://app.m.kuaishou.com/verify/captcha.html",
        }
        payload = {"type": request_type, "uri": uri, "key": key, "input": ticket}
        body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
        return self.session.post(CAPTCHA_VERIFY_ENDPOINT, headers=headers, data=body, timeout=self.timeout)

    def request(self, method: str, path_or_url: str, **kwargs: Any) -> Any:
        signed = self.signed_request(method, path_or_url, **kwargs)
        return self.send(signed)

    def endpoint_names(self, *, safe_only: bool = False) -> list[str]:
        names = sorted(REST_ENDPOINTS)
        if safe_only:
            names = [name for name in names if REST_ENDPOINTS[name].safe]
        return names

    def rest_endpoint(self, name: str) -> RestEndpoint:
        try:
            return REST_ENDPOINTS[name]
        except KeyError as exc:
            known = ", ".join(self.endpoint_names())
            raise ValueError(f"unknown REST endpoint {name!r}; known: {known}") from exc

    def rest_request(
        self,
        name: str,
        *,
        data: Mapping[str, Any] | None = None,
        params: Mapping[str, Any] | None = None,
        include_auth: bool | None = None,
        sig3_mode: str | None = None,
    ) -> SignedRequest:
        endpoint = self.rest_endpoint(name)
        path = endpoint.path
        merged_data = _stringify_mapping(endpoint.defaults)
        merged_params = _stringify_mapping(endpoint.query_defaults)
        merged_params.update(_stringify_mapping(params))
        if name == "startup" and "extId" in merged_data:
            merged_params.setdefault("extId", merged_data.pop("extId"))
        elif name == "plugin_startup" and "extId" in merged_data:
            merged_params.setdefault("extId", merged_data.pop("extId"))
        if name == "refresh_token":
            merged_data.update({"token": self.device.token, "client_salt": self.device.client_salt})
        elif name == "logout":
            merged_data.update({"token": self.device.token, "client_salt": self.device.client_salt})
        if data:
            merged_data.update(_stringify_mapping(data))
        if name == "video_photo_info" and not merged_data.get("photoIds") and merged_data.get("photoId"):
            merged_data["photoIds"] = str(merged_data.pop("photoId"))
        if path == "@Url":
            path = str(merged_params.pop("url", "") or merged_data.pop("url", ""))
            if not path:
                raise ValueError(f"REST endpoint {name!r} requires a url=... value for Retrofit @Url")
        elif not urllib.parse.urlsplit(path).scheme and not path.startswith("/rest/"):
            path = "/rest/" + path.lstrip("/")
        auth = endpoint.include_auth if include_auth is None else include_auth
        method = endpoint.method.upper()
        if method == "GET":
            merged_params = {**merged_data, **merged_params}
            merged_data = {}
        return self.signed_request(
            method,
            path,
            params=merged_params,
            data=merged_data if method != "GET" else None,
            sign_body=method != "GET",
            include_auth=auth,
            sig3_mode=sig3_mode,
            param_profile=endpoint.param_profile,
            rewrite_path=endpoint.rewrite_path,
        )

    def call_rest(self, name: str, **kwargs: Any) -> Any:
        signed = self.rest_request(name, **kwargs)
        return self.send(signed, capture_dynamic=True)

    def api(
        self,
        name: str,
        *,
        data: Mapping[str, Any] | None = None,
        params: Mapping[str, Any] | None = None,
        include_auth: bool | None = None,
        sig3_mode: str | None = None,
        send: bool = True,
        **fields: Any,
    ) -> Any:
        endpoint = self.rest_endpoint(name)
        body_data = _stringify_mapping(data)
        query_params = _stringify_mapping(params)
        for key, value in fields.items():
            if key in endpoint.query_fields or (endpoint.method.upper() == "GET" and key not in endpoint.fields):
                query_params[key] = _stringify_value(value)
            else:
                body_data[key] = _stringify_value(value)
        signed = self.rest_request(
            name,
            data=body_data,
            params=query_params,
            include_auth=include_auth,
            sig3_mode=sig3_mode,
        )
        if not send:
            return signed
        return self.send(signed, capture_dynamic=True)

    def main_app_rest_request(
        self,
        name: str,
        *,
        data: Mapping[str, Any] | None = None,
        params: Mapping[str, Any] | None = None,
        include_auth: bool | None = None,
        sig3_mode: str | None = None,
        rewrite_path: bool = True,
    ) -> SignedRequest:
        endpoint = self.rest_endpoint(name)
        path = endpoint.path
        merged_data = _stringify_mapping(endpoint.defaults)
        merged_params = _stringify_mapping(endpoint.query_defaults)
        merged_params.update(_stringify_mapping(params))
        if data:
            merged_data.update(_stringify_mapping(data))
        if path == "@Url":
            path = str(merged_params.pop("url", "") or merged_data.pop("url", ""))
            if not path:
                raise ValueError(f"REST endpoint {name!r} requires a url=... value for Retrofit @Url")
        elif not urllib.parse.urlsplit(path).scheme and not path.startswith("/rest/"):
            path = "/rest/" + path.lstrip("/")
        auth = endpoint.include_auth if include_auth is None else include_auth
        method = endpoint.method.upper()
        if method == "GET":
            merged_params = {**merged_data, **merged_params}
            merged_data = {}
        return self.signed_request(
            method,
            path,
            params=merged_params,
            data=merged_data if method != "GET" else None,
            sign_body=method != "GET",
            include_auth=auth,
            sig3_mode=sig3_mode,
            param_profile="main_video",
            rewrite_path=rewrite_path,
        )

    def call_main_app_rest(self, name: str, **kwargs: Any) -> Any:
        signed = self.main_app_rest_request(name, **kwargs)
        return self.send(signed, capture_dynamic=True)

    def video_feed_hot(self, count: int | str = 1, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"count": str(count), "pcursor": pcursor}
        if extra:
            data.update(_stringify_mapping(extra))
        return self.call_rest("video_feed_hot", data=data)

    def video_feed_profile(
        self,
        user_id: str,
        count: int | str = 1,
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"user_id": user_id, "count": str(count), "pcursor": pcursor}
        if extra:
            data.update(_stringify_mapping(extra))
        return self.call_rest("video_feed_profile", data=data)

    def video_photo_info(
        self,
        photo_id: str,
        *,
        user_id: str = "",
        exp_tag: str = "",
        llsid: str = "",
        photo_index: int | str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"photoIds": photo_id}
        if user_id:
            data.setdefault("preUserId", str(user_id))
        if exp_tag:
            data.setdefault("preExpTag", str(exp_tag))
        if llsid:
            data.setdefault("preLLSId", str(llsid))
        if photo_index is not None:
            data.setdefault("prePhotoIndex", str(photo_index))
        if extra:
            data.update(_stringify_mapping(extra))
        return self.call_rest("video_photo_info", data=data)

    def video_photo_comments(
        self,
        photo_id: str,
        count: int | str = 20,
        order: str = "desc",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"photoId": photo_id, "count": str(count), "order": order, "enableEmotion": "true"}
        if extra:
            data.update(_stringify_mapping(extra))
        return self.call_rest("video_photo_comments", data=data)

    def api_user_info(self, user_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
        data = {"userIds": _stringify_value(user_ids)}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_user_info", data=data)

    def api_get_users_profile_batch(self, user_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
        data = {"userIds": _stringify_value(user_ids)}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_get_users_profile_batch", data=data)

    def api_get_photo_infos(self, photo_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
        data = {"photoIds": _stringify_value(photo_ids)}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_get_photo_infos", data=data)

    def api_comment_list_v2(
        self,
        photo_id: str,
        *,
        user_id: str = "",
        order: str = "desc",
        pcursor: str = "",
        count: int | str = 20,
        photo_page_type: int | str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "token": self.device.token,
            "photoId": photo_id,
            "user_id": user_id,
            "order": order,
            "pcursor": pcursor,
            "count": str(count),
            "photoPageType": str(photo_page_type),
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_comment_list_v2", data=data)

    def api_comment_sub_list(
        self,
        photo_id: str,
        root_comment_id: str,
        *,
        user_id: str = "",
        order: str = "desc",
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "token": self.device.token,
            "photoId": photo_id,
            "user_id": user_id,
            "order": order,
            "pcursor": pcursor,
            "rootCommentId": root_comment_id,
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_comment_sub_list", data=data)

    def api_comment_list_by_pivot(
        self,
        photo_id: str,
        root_comment_id: str,
        comment_id: str,
        *,
        user_id: str = "",
        order: str = "desc",
        pcursor: str = "",
        filter_sub_comment: bool = False,
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "token": self.device.token,
            "photoId": photo_id,
            "user_id": user_id,
            "order": order,
            "pcursor": pcursor,
            "rootCommentId": root_comment_id,
            "commentId": comment_id,
            "filterSubComment": filter_sub_comment,
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_comment_list_by_pivot", data=data)

    def api_search(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search", data=data)

    def api_search_user(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_user", data=data)

    def api_search_tag(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_tag", data=data)

    def api_search_suggest(self, keyword: str, extra: Mapping[str, Any] | None = None) -> Any:
        data = {"keyword": keyword}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_suggest", data=data)

    def api_search_home(self, pcursor: str = "", prsid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"pcursor": pcursor, "prsid": prsid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_home", data=data)

    def api_search_recommend(self, pcursor: str = "", prsid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"pcursor": pcursor, "prsid": prsid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_recommend", data=data)

    def api_search_kwai_hot_billboard(self, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"pcursor": pcursor}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_search_kwai_hot_billboard", data=data)

    def api_profile_feed(
        self,
        user_id: str,
        *,
        lang: str = "zh-cn",
        count: int | str = 20,
        privacy: str = "",
        pcursor: str = "",
        referer: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "token": self.device.token,
            "user_id": user_id,
            "lang": lang,
            "count": str(count),
            "privacy": privacy,
            "pcursor": pcursor,
            "referer": referer,
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_profile_feed", data=data)

    def api_user_profile_v2(self, user: str, ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"user": user, "token": self.device.token, "ussid": ussid}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_user_profile_v2", data=data)

    def api_feed_tag(
        self,
        tag: str,
        *,
        count: int | str = 20,
        pcursor: str = "",
        ussid: str = "",
        tag_source: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"tag": tag, "count": str(count), "pcursor": pcursor, "ussid": ussid, "tagSource": tag_source}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_feed_tag", data=data)

    def api_tag_detail(self, tag: str, extra: Mapping[str, Any] | None = None) -> Any:
        data = {"tag": tag}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_tag_detail", data=data)

    def api_likers(self, photo_id: str, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"photo_id": photo_id, "pcursor": pcursor}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_likers", data=data)

    def api_photo_check_filter(self, photo_id: str, extra: Mapping[str, Any] | None = None) -> Any:
        data = {"photoId": photo_id}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_check_photo", data=data)

    def api_ad_list(self, extra: Mapping[str, Any] | None = None) -> Any:
        data = _stringify_mapping(extra)
        return self.call_main_app_rest("api_ad_list", data=data)

    def api_promotion_widget(self, extra: Mapping[str, Any] | None = None) -> Any:
        params = _stringify_mapping(extra)
        return self.call_rest("api_promotion_widget", params=params)

    def api_resource_meta(self, name: str = "android.json", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"name": name}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_update_config", data=data)

    def api_system_dialog(self, source: str = "startup", extra: Mapping[str, Any] | None = None) -> Any:
        data = {"source": source}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("system_dialog", data=data)

    def api_live_auth_status(self, extra: Mapping[str, Any] | None = None) -> Any:
        data = _stringify_mapping(extra)
        return self.call_main_app_rest("api_live_auth_status", data=data)

    def api_follow_users(
        self,
        touid: str = "0",
        *,
        ftype: int | str = 1,
        page: int | str = "",
        pcursor: str = "",
        latest_insert_time: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "touid": touid,
            "ftype": ftype,
            "page": page,
            "pcursor": pcursor,
            "latest_insert_time": latest_insert_time,
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_get_follow_users", data=data)

    def api_message_dialog(
        self,
        count: int | str = 20,
        page: int | str = "",
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"token": self.device.token, "count": str(count), "page": str(page), "pcursor": pcursor}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_message_dialog", data=data)

    def api_message_load(
        self,
        user_id: str,
        page: int | str = "",
        order: str = "desc",
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"page": str(page), "token": self.device.token, "user_id": user_id, "order": order, "pcursor": pcursor}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_message_load", data=data)

    def api_send_message(
        self,
        user_id: str,
        content: str,
        *,
        copy: bool | str = False,
        photo_id: str = "",
        live_stream_id: str = "",
        informed_user_id: str = "",
        extra: Mapping[str, Any] | None = None,
        send: bool = True,
    ) -> Any:
        data = {
            "user_id": user_id,
            "content": content,
            "copy": copy,
            "photoId": photo_id,
            "liveStreamId": live_stream_id,
            "informedUserId": informed_user_id,
        }
        data.update(_stringify_mapping(extra))
        signed = self.main_app_rest_request("api_send_message", data=data)
        if not send:
            return signed
        return self.send(signed, capture_dynamic=True)

    def private_chat_list(
        self,
        count: int | str = 20,
        page: int | str = "",
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        return self.api_message_dialog(count=count, page=page, pcursor=pcursor, extra=extra)

    def private_chat_messages(
        self,
        user_id: str,
        page: int | str = "",
        order: str = "desc",
        pcursor: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        return self.api_message_load(user_id=user_id, page=page, order=order, pcursor=pcursor, extra=extra)

    def private_chat_send(
        self,
        user_id: str,
        content: str,
        *,
        copy: bool | str = False,
        photo_id: str = "",
        live_stream_id: str = "",
        informed_user_id: str = "",
        extra: Mapping[str, Any] | None = None,
        send: bool = True,
    ) -> Any:
        return self.api_send_message(
            user_id,
            content,
            copy=copy,
            photo_id=photo_id,
            live_stream_id=live_stream_id,
            informed_user_id=informed_user_id,
            extra=extra,
            send=send,
        )

    def api_get_feed_selection(
        self,
        *,
        page: int | str = 1,
        cold: bool | str = "",
        cold_start: bool | str = False,
        count: int | str = 20,
        pcursor: str = "",
        pv: str = "false",
        photo_infos: str = "",
        new_user_refresh_times: int | str = 0,
        new_user_action: str = "",
        reco_report_context: str = "",
        source: int | str = 1,
        edge_reco_bit: int | str = 0,
        real_show_photo_ids: str = "",
        extra: Mapping[str, Any] | None = None,
        query_extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "page": page,
            "coldStart": cold_start,
            "count": count,
            "pcursor": pcursor,
            "pv": pv,
            "photoInfos": photo_infos,
            "newUserRefreshTimes": new_user_refresh_times,
            "newUserAction": new_user_action,
            "recoReportContext": reco_report_context,
            "source": source,
            "edgeRecoBit": edge_reco_bit,
            "realShowPhotoIds": real_show_photo_ids,
        }
        data.update(_stringify_mapping(extra))
        params = {"cold": cold}
        params.update(_stringify_mapping(query_extra))
        return self.call_main_app_rest("api_get_feed_selection", data=data, params=params)

    def api_feed_like_list(
        self,
        user_id: str,
        *,
        count: int | str = 20,
        pcursor: str = "",
        referer: str = "",
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {"token": self.device.token, "id": user_id, "count": str(count), "pcursor": pcursor, "referer": referer}
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_feed_like_list", data=data)

    def api_feed_my_follow(
        self,
        *,
        feed_type: int | str = 0,
        page: int | str = 1,
        count: int | str = 20,
        feed_id: int | str = 0,
        pcursor: str = "",
        refresh_times: int | str = 0,
        cold_start: bool | str = False,
        source: int | str = 1,
        my_follow_slide_type: int | str = 0,
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        data = {
            "type": feed_type,
            "page": page,
            "token": self.device.token,
            "count": count,
            "id": feed_id,
            "pcursor": pcursor,
            "refreshTimes": refresh_times,
            "coldStart": cold_start,
            "source": source,
            "myFollowSlideType": my_follow_slide_type,
        }
        data.update(_stringify_mapping(extra))
        return self.call_main_app_rest("api_feed_my_follow", data=data)

    @staticmethod
    def extract_video_ids(payload: Any) -> list[dict[str, str]]:
        items: list[dict[str, str]] = []
        if not isinstance(payload, Mapping):
            return items
        feeds = payload.get("feeds")
        if not isinstance(feeds, list):
            return items
        for feed in feeds:
            if not isinstance(feed, Mapping):
                continue
            photo_id = feed.get("photo_id") or feed.get("photoId") or feed.get("id")
            user_id = feed.get("user_id") or feed.get("userId")
            feed_log_ctx = feed.get("feedLogCtx")
            if photo_id:
                items.append(
                    {
                        "photo_id": str(photo_id),
                        "user_id": "" if user_id is None else str(user_id),
                        "exp_tag": "" if feed.get("exp_tag") is None else str(feed.get("exp_tag")),
                        "server_exp_tag": "" if feed.get("serverExpTag") is None else str(feed.get("serverExpTag")),
                        "llsid": "" if payload.get("llsid") is None else str(payload.get("llsid")),
                        "stid_container": ""
                        if not isinstance(feed_log_ctx, Mapping) or feed_log_ctx.get("stidContainer") is None
                        else str(feed_log_ctx.get("stidContainer")),
                    }
                )
        return items

    @staticmethod
    def response_summary(name: str, response: Any, *, preview_len: int = 360) -> dict[str, Any]:
        text = getattr(response, "text", "") or ""
        payload = _response_json(response)
        result_code = payload.get("result") if isinstance(payload, Mapping) else None
        error_msg = str(payload.get("error_msg", "")) if isinstance(payload, Mapping) else ""
        return {
            "name": name,
            "status_code": getattr(response, "status_code", None),
            "result": result_code,
            "signature_ok": not (result_code == 50 and ("??" in error_msg or "signature" in error_msg.lower())),
            "error_msg": error_msg,
            "body_preview": text[:preview_len],
        }

    def info_endpoint_names(self, category: str = "all") -> list[str]:
        if category == "all":
            names = INFO_REST_ENDPOINT_NAMES
        else:
            try:
                names = REST_INFO_CATEGORIES[category]
            except KeyError as exc:
                known = ", ".join(["all", *sorted(REST_INFO_CATEGORIES)])
                raise ValueError(f"unknown info REST category {category!r}; known: {known}") from exc
        return sorted(name for name in names if name in REST_ENDPOINTS)

    def _record_info_call(
        self,
        results: list[dict[str, Any]],
        name: str,
        *,
        data: Mapping[str, Any] | None = None,
        params: Mapping[str, Any] | None = None,
        preview_len: int = 360,
    ) -> Any:
        try:
            response = self.call_rest(name, data=data, params=params)
            results.append(self.response_summary(name, response, preview_len=preview_len))
            return response
        except Exception as exc:
            results.append({"name": name, "error": str(exc)})
            return None

    def _first_feed_identity(self, count: int | str = 1) -> dict[str, str]:
        response = self.video_feed_hot(count=count)
        payload = _response_json(response)
        extracted = self.extract_video_ids(payload)
        return extracted[0] if extracted else {}

    def smoke_test_info_rest(
        self,
        *,
        category: str = "all",
        count: int | str = 1,
        keyword: str = "猫",
    ) -> list[dict[str, Any]]:
        categories = sorted(REST_INFO_CATEGORIES) if category == "all" else [category]
        results: list[dict[str, Any]] = []
        identity: dict[str, str] = {}

        for item in categories:
            if item not in REST_INFO_CATEGORIES:
                known = ", ".join(["all", *sorted(REST_INFO_CATEGORIES)])
                raise ValueError(f"unknown info REST category {item!r}; known: {known}")
            if item == "search":
                if keyword:
                    self._record_info_call(results, "api_search_suggest", data={"keyword": keyword})
                    self._record_info_call(results, "api_search", data={"keyword": keyword, "pcursor": "", "ussid": ""})
                    self._record_info_call(results, "api_search_user", data={"keyword": keyword, "pcursor": "", "ussid": ""})
                    self._record_info_call(results, "api_search_tag", data={"keyword": keyword, "pcursor": "", "ussid": ""})
                self._record_info_call(results, "api_search_home", data={"pcursor": "", "prsid": ""})
                self._record_info_call(results, "api_search_recommend", data={"pcursor": "", "prsid": ""})
                self._record_info_call(results, "api_search_kwai_hot_billboard", data={"pcursor": ""})
                self._record_info_call(results, "api_search_tag_recommend", data={"pcursor": ""})
            elif item == "video":
                feed_response = self._record_info_call(results, "video_feed_hot", data={"count": str(count), "pcursor": ""})
                feed_payload = _response_json(feed_response) if feed_response is not None else None
                extracted = self.extract_video_ids(feed_payload)
                if extracted:
                    identity = extracted[0]
                photo_id = identity.get("photo_id", "")
                user_id = identity.get("user_id", "")
                self._record_info_call(results, "api_get_hot_items", data={"count": str(count), "pcursor": "", "page": "1"})
                self._record_info_call(results, "api_get_feed_selection", data={"count": str(count), "page": "1", "pcursor": ""})
                if photo_id:
                    self._record_info_call(results, "api_get_photo_infos", data={"photoIds": photo_id})
                    self._record_info_call(results, "video_photo_info", data={"photoIds": photo_id})
                    self._record_info_call(results, "api_comment_list_v2", data={"photoId": photo_id, "user_id": user_id, "count": str(count), "order": "desc"})
                    self._record_info_call(results, "video_photo_comments", data={"photoId": photo_id, "count": str(count), "order": "desc", "enableEmotion": "true"})
                    self._record_info_call(results, "api_likers", data={"photo_id": photo_id, "pcursor": ""})
                if user_id:
                    self._record_info_call(results, "video_feed_profile", data={"user_id": user_id, "count": str(count), "pcursor": ""})
                if keyword:
                    self._record_info_call(results, "api_feed_tag", data={"tag": keyword, "count": str(count), "pcursor": ""})
                    self._record_info_call(results, "api_tag_detail", data={"tag": keyword})
            elif item == "user":
                if not identity:
                    identity = self._first_feed_identity(count=count)
                user_id = identity.get("user_id", "")
                if user_id:
                    self._record_info_call(results, "api_user_info", data={"userIds": user_id})
                    self._record_info_call(results, "api_get_users_profile_batch", data={"userIds": user_id})
                    self._record_info_call(results, "api_user_profile_v2", data={"user": user_id, "token": self.device.token, "ussid": ""})
                    self._record_info_call(results, "api_profile_feed", data={"user_id": user_id, "token": self.device.token, "count": str(count), "pcursor": ""})
                    self._record_info_call(results, "api_profile_list_friends", data={"user": user_id, "count": str(count), "pcursor": ""})
                    self._record_info_call(results, "api_get_follow_users", data={"touid": user_id, "page": "1", "pcursor": ""})
                    self._record_info_call(results, "api_get_relation_friends", data={"touid": user_id, "pcursor": ""})
                self._record_info_call(results, "api_get_friend_users", data={"lastModified": ""})
                self._record_info_call(results, "api_get_at_users", data={"keyword": keyword, "pcursor": ""})
                self._record_info_call(results, "user_profile", data={})
                self._record_info_call(results, "user_settings", data={})
            elif item == "observed":
                if not identity:
                    identity = self._first_feed_identity(count=count)
                photo_id = identity.get("photo_id", "")
                user_id = identity.get("user_id", "")
                self._record_info_call(results, "api_promotion_widget", params={}, preview_len=160)
                self._record_info_call(results, "api_ad_list", data={}, preview_len=160)
                self._record_info_call(results, "api_update_config", data={"name": "android.json"}, preview_len=160)
                self._record_info_call(results, "system_dialog", data={"source": "startup"}, preview_len=160)
                self._record_info_call(results, "api_live_auth_status", data={}, preview_len=160)
                if photo_id:
                    self._record_info_call(results, "api_check_photo", data={"photoId": photo_id}, preview_len=160)
                if user_id:
                    self._record_info_call(results, "api_get_follow_users", data={"touid": user_id, "ftype": "1"}, preview_len=160)
            elif item == "message":
                self._record_info_call(results, "api_message_dialog", data={"token": self.device.token, "count": str(count), "page": "", "pcursor": ""}, preview_len=160)
                self._record_info_call(results, "api_message_load", data={"page": "", "token": self.device.token, "user_id": "", "order": "desc", "pcursor": ""}, preview_len=160)
                self._record_info_call(results, "api_news_load", data={"token": self.device.token, "count": str(count), "page": "", "pcursor": ""}, preview_len=160)
                self._record_info_call(results, "api_notify_load", data={"token": self.device.token, "subVersion": "", "pcursor": "", "latest_insert_time": ""}, preview_len=160)
                self._record_info_call(results, "api_nebula_notify_load", data={"token": self.device.token, "subVersion": "", "pcursor": "", "latest_insert_time": ""}, preview_len=160)
                self._record_info_call(results, "api_get_share_user_list", data={}, preview_len=160)
                self._record_info_call(results, "api_get_friend_users", data={"lastModified": ""}, preview_len=160)
        return results

    def build_rest_probe(self, names: Iterable[str] | None = None) -> list[dict[str, Any]]:
        selected = list(names or self.endpoint_names())
        results: list[dict[str, Any]] = []
        for name in selected:
            endpoint = self.rest_endpoint(name)
            data = {field: f"{field}-demo" for field in endpoint.fields}
            params = {field: f"{field}-demo" for field in endpoint.query_fields}
            if endpoint.path == "@Url":
                data["url"] = "https://example.test/probe"
            try:
                signed = self.rest_request(name, data=data, params=params)
                sig_location = "params" if signed.params.get("__NS_sig3") else "data"
                results.append(
                    {
                        "name": name,
                        "ok": True,
                        "method": signed.method,
                        "url": signed.url,
                        "include_auth": endpoint.include_auth,
                        "param_profile": endpoint.param_profile,
                        "sig3_location": sig_location,
                    }
                )
            except Exception as exc:
                results.append({"name": name, "ok": False, "error": str(exc)})
        return results

    def smoke_test_public_apis(self, *, count: int | str = 1, keyword: str = "??") -> list[dict[str, Any]]:
        results: list[dict[str, Any]] = []

        def record(name: str, response: Any) -> None:
            results.append(self.response_summary(name, response))

        feed_response = self.video_feed_hot(count=count)
        record("video_feed_hot", feed_response)
        feed_payload = _response_json(feed_response)
        extracted = self.extract_video_ids(feed_payload)
        if extracted:
            first = extracted[0]
            photo_id = first.get("photo_id", "")
            user_id = first.get("user_id", "")
            exp_tag = first.get("exp_tag", "")
            llsid = first.get("llsid", "")
            if photo_id:
                record("api_get_photo_infos", self.api_get_photo_infos(photo_id))
                record("video_photo_info", self.video_photo_info(photo_id, user_id=user_id, exp_tag=exp_tag, llsid=llsid, photo_index=0))
                record("api_comment_list_v2", self.api_comment_list_v2(photo_id, user_id=user_id, count=1))
                record("video_photo_comments", self.video_photo_comments(photo_id, count=1))
            if user_id:
                record("api_user_info", self.api_user_info(user_id))
                record("api_user_profile_v2", self.api_user_profile_v2(user_id))
                record("api_profile_feed", self.api_profile_feed(user_id, count=1))
        if keyword:
            record("api_search_suggest", self.api_search_suggest(keyword))
            record("api_search", self.api_search(keyword))
        return results

    def smoke_test_rest(self, names: Iterable[str] | None = None) -> list[dict[str, Any]]:
        selected = list(names or DEFAULT_REST_SMOKE_ENDPOINT_NAMES)
        results: list[dict[str, Any]] = []
        for name in selected:
            endpoint = self.rest_endpoint(name)
            if not endpoint.safe:
                results.append({"name": name, "skipped": True, "reason": "endpoint is not marked safe"})
                continue
            try:
                response = self.call_rest(name)
                text = getattr(response, "text", "") or ""
                payload = _response_json(response)
                result_code = payload.get("result") if isinstance(payload, Mapping) else None
                error_msg = str(payload.get("error_msg", "")) if isinstance(payload, Mapping) else ""
                results.append(
                    {
                        "name": name,
                        "status_code": getattr(response, "status_code", None),
                        "result": result_code,
                        "signature_ok": not (result_code == 50 and ("签名" in error_msg or "signature" in error_msg.lower())),
                        "body_preview": text[:240],
                    }
                )
            except Exception as exc:
                results.append({"name": name, "error": str(exc)})
        return results

    def get(self, path_or_url: str, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
        return self.request("GET", path_or_url, params=params, sign_body=False, **kwargs)

    def post_form(self, path_or_url: str, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
        return self.request("POST", path_or_url, data=data, sign_body=True, **kwargs)

    def login(
        self,
        path_or_url: str,
        data: Mapping[str, Any] | None = None,
        *,
        params: Mapping[str, Any] | None = None,
        method: str = "POST",
        include_auth: bool = False,
        param_profile: str = "main_video_login",
        rewrite_path: bool = True,
        **kwargs: Any,
    ) -> Any:
        sig3_mode = kwargs.pop("sig3_mode", None)
        signed = self.signed_request(
            method,
            path_or_url,
            params=params,
            data=data,
            sign_body=method.upper() != "GET",
            include_auth=include_auth,
            sig3_mode=sig3_mode,
            param_profile=param_profile,
            rewrite_path=rewrite_path,
        )
        return self.send(signed, capture_dynamic=True, **kwargs)

    def login_mobile_check(self, mobile: str, country_code: str = "86", **kwargs: Any) -> Any:
        return self.login(
            "/rest/n/user/mobile/checker",
            {"mobileCountryCode": country_code, "mobile": mobile},
            include_auth=False,
            rewrite_path=False,
            **kwargs,
        )

    def request_mobile_code(self, mobile: str, country_code: str = "86", code_type: int = 27, **kwargs: Any) -> Any:
        return self.login(
            "/rest/n/user/requestMobileCode",
            {"mobileCountryCode": country_code, "mobile": mobile, "type": str(code_type)},
            include_auth=False,
            rewrite_path=False,
            **kwargs,
        )

    def verify_code_login(
        self,
        mobile: str,
        code: str,
        country_code: str = "86",
        code_type: int = 27,
        extra: Mapping[str, Any] | None = None,
        use_account_keypair: bool = True,
        include_public_key: bool = False,
        **kwargs: Any,
    ) -> Any:
        form = {
            "code": code,
            "mobileCountryCode": country_code,
            "mobile": mobile,
            "type": str(code_type),
        }
        if extra:
            form.update(_stringify_mapping(extra))
        if use_account_keypair:
            if include_public_key and not form.get("publicKey"):
                form["publicKey"] = self.account_public_key()
                form.setdefault("deviceName", self.device.mod)
                form.setdefault("deviceMode", self.device.mod)
            self.add_account_security_fields(form)
        return self.login("/rest/n/user/login/mobileVerifyCode", form, include_auth=False, rewrite_path=False, **kwargs)

    def phone_code_login(
        self,
        mobile: str,
        code: str,
        country_code: str = "86",
        code_type: int = 27,
        extra: Mapping[str, Any] | None = None,
        use_account_keypair: bool = True,
        include_public_key: bool = False,
        **kwargs: Any,
    ) -> Any:
        return self.verify_code_login(
            mobile,
            code,
            country_code=country_code,
            code_type=code_type,
            extra=extra,
            use_account_keypair=use_account_keypair,
            include_public_key=include_public_key,
            **kwargs,
        )

    def register_by_phone_code_login(
        self,
        mobile: str,
        code: str,
        country_code: str = "86",
        extra: Mapping[str, Any] | None = None,
        use_account_keypair: bool = True,
        **kwargs: Any,
    ) -> Any:
        form = {
            "mobileCountryCode": country_code,
            "mobile": mobile,
            "mobileCode": code,
            "publicKey": self.account_public_key(),
            "deviceName": self.device.mod,
            "deviceMode": self.device.mod,
            "type": "302",
        }
        if extra:
            form.update(_stringify_mapping(extra))
        if use_account_keypair:
            self.add_account_security_fields(form)
        return self.login("/rest/n/user/register/mobileV2", form, include_auth=False, rewrite_path=False, **kwargs)

    def token_login(
        self,
        token: str,
        extra: Mapping[str, Any] | None = None,
        *,
        use_account_keypair: bool = True,
        **kwargs: Any,
    ) -> Any:
        form = {"token": token, "type": "302", "deviceName": self.device.mod, "deviceMode": self.device.mod}
        if extra:
            form.update(_stringify_mapping(extra))
        if use_account_keypair:
            if not form.get("publicKey"):
                form["publicKey"] = self.account_public_key()
            self.add_account_security_fields(form)
        return self.login("/rest/n/user/login/token", form, include_auth=False, **kwargs)

    def third_platform_login(
        self,
        platform: str,
        access_token: str,
        open_id: str = "",
        extra: Mapping[str, Any] | None = None,
        *,
        use_account_keypair: bool = True,
        **kwargs: Any,
    ) -> Any:
        form = {"platform": platform, "accessToken": access_token, "openId": open_id}
        form.update(_stringify_mapping(extra))
        if use_account_keypair:
            if not form.get("publicKey"):
                form["publicKey"] = self.account_public_key()
            self.add_account_security_fields(form)
        return self.login("/rest/user/thirdPlatformLogin", form, include_auth=False, rewrite_path=False, **kwargs)

    def auth_wechat_code(self, code: str, extra: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
        form = {"code": code}
        form.update(_stringify_mapping(extra))
        return self.login("/rest/n/wechat/oauth2/authByCode", form, include_auth=False, **kwargs)

    def verify_trust_device(
        self,
        is_add_account: bool | str = False,
        extra: Mapping[str, Any] | None = None,
        **kwargs: Any,
    ) -> Any:
        form = {"isAddAccount": is_add_account}
        form.update(_stringify_mapping(extra))
        return self.login("/rest/n/user/verifyTrustDevice", form, include_auth=True, **kwargs)

    def account_public_key(self) -> str:
        private_key = self._load_or_create_account_private_key()
        from cryptography.hazmat.primitives import serialization

        public_der = private_key.public_key().public_bytes(
            encoding=serialization.Encoding.DER,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        return base64.b64encode(public_der).decode("ascii")

    def add_account_security_fields(self, form: dict[str, str], *, now_ms: int | None = None) -> None:
        if form.get("raw") and form.get("secret"):
            return
        raw = str(now_ms if now_ms is not None else int(time.time() * 1000))
        form["raw"] = raw
        form["secret"] = self.sign_account_raw(raw)

    def sign_account_raw(self, raw: str) -> str:
        private_key = self._load_or_create_account_private_key()
        from cryptography.hazmat.primitives import hashes
        from cryptography.hazmat.primitives.asymmetric import padding

        signature = private_key.sign(raw.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
        return base64.b64encode(signature).decode("ascii")

    def _load_or_create_account_private_key(self) -> Any:
        try:
            from cryptography.hazmat.primitives import serialization
            from cryptography.hazmat.primitives.asymmetric import rsa
        except Exception as exc:
            raise RuntimeError("cryptography is required for account raw/secret RSA fields") from exc
        if self.device.account_private_key_pem:
            return serialization.load_pem_private_key(self.device.account_private_key_pem.encode("utf-8"), password=None)
        private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        self.device.account_private_key_pem = private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        ).decode("ascii")
        return private_key

    def register_by_phone_v2(
        self,
        mobile: str,
        code: str,
        password: str,
        user_name: str = "",
        gender: str = "U",
        country_code: str = "86",
        extra: Mapping[str, Any] | None = None,
        **kwargs: Any,
    ) -> Any:
        form = {
            "userName": user_name,
            "mobileCountryCode": country_code,
            "mobile": mobile,
            "mobileCode": code,
            "password": password,
            "gender": gender,
        }
        if extra:
            form.update(_stringify_mapping(extra))
        return self.login("/rest/n/user/register/mobileV2", form, include_auth=False, **kwargs)

    def refresh_token(self, extra: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
        form = {
            "token": self.device.token,
            "client_salt": self.device.client_salt,
        }
        if extra:
            form.update(_stringify_mapping(extra))
        return self.login("/rest/n/token/infra/refreshToken", form, include_auth=True, param_profile="antman", **kwargs)

    def logout(self, **kwargs: Any) -> Any:
        return self.login(
            "/rest/n/user/logout",
            {"token": self.device.token, "client_salt": self.device.client_salt},
            include_auth=True,
            param_profile="antman",
            **kwargs,
        )

    def startup(self, ext_id: str = "", gp_referer: str = "", oaid: str = "", **kwargs: Any) -> Any:
        signed = self.signed_request(
            "POST",
            "/rest/system/startup",
            params={"extId": ext_id},
            data={"gp_referer": gp_referer, "oaid": oaid},
            sign_body=True,
            include_auth=True,
        )
        return self.send(signed, capture_dynamic=True, **kwargs)

    def version_check(self, **kwargs: Any) -> Any:
        return self.get("/rest/antman/versionCheck", **kwargs)

    def capture_dynamic_fields(self, response: Any, *, request_path: str = "") -> dict[str, str]:
        changed: dict[str, str] = {}
        cookies = _response_cookies(response)
        if cookies:
            changed.update(self.device.update_from_mapping(cookies, overwrite=True))
            extras = {key: value for key, value in cookies.items() if key not in DEVICE_ALIAS_NAMES and value}
            self.device.cookie_extras.update(extras)
        payload = _response_json(response)
        if payload is not None:
            is_auth_response = any(marker in request_path for marker in ("/user/login/", "/token/infra/refreshToken"))
            aliases = DEVICE_FIELD_ALIASES if is_auth_response else SAFE_RESPONSE_FIELD_ALIASES
            for mapping in _high_confidence_response_mappings(payload):
                changed.update(self.device.update_from_mapping(mapping, overwrite=True, aliases=aliases, recursive=False))
        return changed

    def dfp_device_info_bytes(self, fields: Mapping[str, Any] | None = None) -> bytes:
        return encode_dfp_proto(build_lite_dfp_fields(fields))

    def dfp_signed_form(
        self,
        field_name: str,
        payload: bytes | str,
        *,
        product_name: str = DFP_PRODUCT_NAME,
        now_ms: int | None = None,
        sv: str = "2",
        include_rdid: bool = True,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
    ) -> dict[str, str]:
        if isinstance(payload, str):
            payload = payload.encode("utf-8")
        if now_ms is None:
            now_ms = int(time.time() * 1000)
        encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
        encoded = _dfp_encode_payload(encrypted)
        ts = str(now_ms)
        sign_input = (product_name + ts + sv + encoded).encode("utf-8")
        sign = _dfp_sign_value(sign_input, self.zt_table, sign_mode)
        if not encrypt_payload and sv == "2":
            sv = "3"
            sign_input = (product_name + ts + sv + encoded).encode("utf-8")
            sign = _dfp_sign_value(sign_input, self.zt_table, sign_mode)
        form = {
            "productName": product_name,
            "ts": ts,
            field_name: encoded,
            "sign": sign,
            "sv": sv,
        }
        if include_rdid and self.device.rdid:
            form["rdid"] = self.device.rdid
            form["didtag"] = self.device.cdid_tag or "-1"
        return form

    def dfp_egid_request(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        endpoint: str = DFP_EGID_ENDPOINT,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
    ) -> SignedRequest:
        form = self.dfp_signed_form(
            "deviceInfo",
            self.dfp_device_info_bytes(fields),
            sign_mode=sign_mode,
            encrypt_payload=encrypt_payload,
        )
        headers = self.base_headers()
        headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
        return SignedRequest("POST", endpoint, headers, {}, form)

    def dfp_did_request(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        endpoint: str = DFP_DID_ENDPOINT,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
    ) -> SignedRequest:
        form = self.dfp_sync_lite_form(fields=fields, sign_mode=sign_mode, encrypt_payload=encrypt_payload)
        headers = self.base_headers()
        headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
        return SignedRequest("POST", endpoint, headers, {}, form)

    def dfp_sync_lite_form(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
        request_id: str = "",
    ) -> dict[str, str]:
        payload = self.dfp_device_info_bytes(fields)
        encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
        encoded = _dfp_encode_payload(encrypted)
        tree: dict[str, str] = {"didTag": "-1"}
        self._add_dfp_fetch_common(tree, include_repair_fields=True)
        tree["deviceInfo"] = encoded
        tree["requestId"] = request_id or _dfp_request_id()
        tree["sv"] = "2" if encrypt_payload else "3"
        sign_input = "".join(value for _, value in _dfp_non_empty_items(tree))
        form = {key: value for key, value in _dfp_non_empty_items(tree)}
        form["sign"] = _dfp_sign_value(sign_input.encode("utf-8"), self.zt_table, sign_mode)
        return form

    def dfp_fetch_form(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
        request_id: str = "",
        hgid_report_id: str = "",
    ) -> dict[str, str]:
        payload = self.dfp_device_info_bytes(fields)
        encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
        encoded = _dfp_encode_payload(encrypted)
        decision_did = self.device.decision_did
        if not decision_did and self.device.cdid_tag:
            decision_did = self.device.did
        decision = self.device.decision
        if not decision and decision_did:
            decision = "8"
        decision_inputs = self.device.decision_inputs
        tree: dict[str, str] = {
            "didTag": self.device.cdid_tag or "-1",
            "odid": self.device.odid or self.device.did,
        }
        self._add_dfp_fetch_common(tree, include_repair_fields=True)
        tree["deviceInfo"] = encoded
        tree["decisionDid"] = decision_did
        tree["decision"] = decision
        tree["decisionInputs"] = decision_inputs
        if hgid_report_id:
            tree["hgidReportId"] = hgid_report_id
        if request_id:
            tree["requestId"] = request_id
        tree["sv"] = "2" if encrypt_payload else "3"
        sign_input = "".join(value for _, value in _dfp_non_empty_items(tree))
        form = {key: value for key, value in _dfp_non_empty_items(tree)}
        form["sign"] = _dfp_sign_value(sign_input.encode("utf-8"), self.zt_table, sign_mode)
        return form

    def _add_dfp_fetch_common(self, form: dict[str, str], *, include_repair_fields: bool = False) -> None:
        product_name = DFP_PRODUCT_NAME or "unknow"
        form["ts"] = str(int(time.time() * 1000))
        form["productName"] = product_name
        form["appVersion"] = "10.6.30.1234" if product_name == "NEBULA" else DFP_APP_VERSION
        form["sdkVersion"] = "8.9.1antman.40.05d13a7f"
        form["platform"] = "1"
        form.setdefault("didTag", self.device.cdid_tag or "-1")
        if include_repair_fields:
            if self.device.rdid:
                form["rdid"] = self.device.rdid
            form["aegon"] = "false"
        form["did"] = self.device.did

    def fetch_egid(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        endpoint: str = DFP_EGID_ENDPOINT,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
        **kwargs: Any,
    ) -> Any:
        return self.send(
            self.dfp_egid_request(fields=fields, endpoint=endpoint, sign_mode=sign_mode, encrypt_payload=encrypt_payload),
            capture_dynamic=True,
            **kwargs,
        )

    def fetch_did(
        self,
        *,
        fields: Mapping[str, Any] | None = None,
        endpoint: str = DFP_DID_ENDPOINT,
        sign_mode: str = "10418",
        encrypt_payload: bool = True,
        **kwargs: Any,
    ) -> Any:
        return self.send(
            self.dfp_did_request(fields=fields, endpoint=endpoint, sign_mode=sign_mode, encrypt_payload=encrypt_payload),
            capture_dynamic=True,
            **kwargs,
        )

    def local_dfp(self, fields: Mapping[str, Any] | None = None, *, encrypt: bool = False) -> str:
        return build_lite_dfp(fields, encrypt=encrypt, table=self.zt_table)

    def encrypt_azeroth_param(self, value: str, security_value: str) -> str:
        return encrypt_param_with_fix(value, security_value)

    def azeroth_client_sign(self, method: str, path: str, params: Mapping[str, Any]) -> str:
        # Azeroth Java uses MXSec atlasSign; for this APK that resolves to the
        # same 10417 sig3 body over SignatureUtil.d(method,path,params,null).
        canonical = azeroth_canonical(method, path, params)
        return sig3_10417(canonical.encode("utf-8"), self.zt_table)

client.py 的几个关键设计理由

  1. RestEndpoint 把 method/path/defaults/safe/profile/rewrite 分离,避免每个接口散落 if/else。
  2. signed_request() 区分 GET 和 POST:GET 签 query,POST 签 body。这个对应 ParamsUtilsz2 分支。
  3. param_profile='auto' 是为了让 antman/main_video/azeroth 不互相污染。
  4. capture-login 导入 HAR 时过滤 captcha token,避免把验证码 token 当登录 token。
  5. 私信发送提供 send=False dry-run,避免误触发真实发消息。

0x09 CLI:复现实验必须有最短入口

如果只有 Python class,每次复现实验都要手写脚本。CLI 的作用就是把关键操作固定成命令:打印请求、导入 HAR、列接口、跑信息类 smoke、私信 dry-run、fetch DID/EGID。

CLI 完整实现:kws_client.py

  • 文件:kws_client.py
  • 行数:760
  • 字节:36555
  • SHA256:efd8ce61d733bfc233737974f071a9302485fca5ebdbfa9e74850ab317f9e443

为什么看它:它把 KwaiClient 的能力暴露成命令,便于复现和调试。

读完得到的结论:它不是算法来源,但它是复现实验入口;文中的命令都来自这里。

from __future__ import annotations

import argparse
import json
import sys
from http.cookies import SimpleCookie
from pathlib import Path

from kws_recovered.client import (
    INFO_REST_ENDPOINT_NAMES,
    REST_ENDPOINTS,
    REST_INFO_CATEGORIES,
    KwaiClient,
    KwaiDeviceContext,
    SignedRequest,
    build_captcha_manual_html,
    parse_captcha_challenge,
)


ROOT = Path(__file__).resolve().parent
DEFAULT_ASSETS = ROOT / "resources" / "assets"
DEFAULT_STATE = ROOT / "kws_device_state.json"
PARAM_PROFILES = [
    "auto",
    "antman",
    "main_video",
    "main_video_login",
    "main_video_compat",
    "main_video_device",
    "azeroth",
    "azeroth_latest",
    "azeroth_device",
]


def parse_cookie_header(cookie_header: str) -> dict[str, str]:
    if not cookie_header:
        return {}
    parsed = SimpleCookie()
    try:
        parsed.load(cookie_header)
        return {key: morsel.value for key, morsel in parsed.items() if morsel.value}
    except Exception:
        out: dict[str, str] = {}
        for item in cookie_header.split(";"):
            if "=" not in item:
                continue
            key, value = item.split("=", 1)
            key = key.strip()
            value = value.strip()
            if key and value:
                out[key] = value
        return out


def parse_pairs(items: list[str]) -> dict[str, str]:
    out: dict[str, str] = {}
    for item in items:
        if "=" not in item:
            raise SystemExit(f"bad key=value argument: {item}")
        key, value = item.split("=", 1)
        out[key] = value
    return out


def print_signed(signed: SignedRequest) -> None:
    print(
        json.dumps(
            {
                "method": signed.method,
                "url": signed.url,
                "headers": signed.headers,
                "params": signed.params,
                "data": signed.data,
            },
            ensure_ascii=False,
            indent=2,
        )
    )


def build_device(args: argparse.Namespace) -> KwaiDeviceContext:
    state_path = Path(args.state)
    if state_path.exists():
        device = KwaiDeviceContext.from_json_file(state_path)
    else:
        device = KwaiDeviceContext()
    overrides = {
        "did": args.did,
        "egid": args.egid,
        "rdid": args.rdid,
        "odid": args.odid,
        "decision_did": args.decision_did,
        "decision": args.decision,
        "decision_inputs": args.decision_inputs,
        "token": args.token,
        "api_st": args.api_st,
        "client_salt": args.client_salt,
        "ud": args.ud,
        "service_token": args.service_token,
        "azeroth_security": args.azeroth_security,
    }
    device.update_from_mapping({key: value for key, value in overrides.items() if value}, overwrite=True)
    cookie_map = parse_cookie_header(args.cookie)
    if cookie_map:
        device.update_from_mapping(cookie_map, overwrite=True)
        device.cookie_extras.update({key: value for key, value in cookie_map.items() if key and value})
    return device


def maybe_save(device: KwaiDeviceContext, args: argparse.Namespace) -> None:
    stateful_actions = {
        "capture-login",
        "login",
        "startup",
        "mobile-check",
        "request-mobile-code",
        "verify-code-login",
        "phone-login",
        "register-phone-code-login",
        "token-login",
        "register-phone-v2",
        "refresh-token",
        "logout",
        "rest-call",
        "rest-smoke",
        "rest-info-smoke",
        "private-chat-list",
        "private-chat-messages",
        "fetch-egid",
        "fetch-did",
    }
    if args.save_state or getattr(args, "send", False) or getattr(args, "action", "") in stateful_actions:
        device.save_json_file(args.state)


def add_common(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--host", default="https://apissl.ksapisrv.com")
    parser.add_argument("--assets", default=str(DEFAULT_ASSETS))
    parser.add_argument("--state", default=str(DEFAULT_STATE), help="device/auth state JSON")
    parser.add_argument("--save-state", action="store_true")
    parser.add_argument("--did", default="")
    parser.add_argument("--egid", default="")
    parser.add_argument("--rdid", default="")
    parser.add_argument("--odid", default="")
    parser.add_argument("--decision-did", default="")
    parser.add_argument("--decision", default="")
    parser.add_argument("--decision-inputs", default="")
    parser.add_argument("--token", default="")
    parser.add_argument("--api-st", default="")
    parser.add_argument("--client-salt", default="")
    parser.add_argument("--ud", default="", help="logged-in user id")
    parser.add_argument("--service-token", default="", help="Azeroth/service token")
    parser.add_argument("--azeroth-security", default="", help="Azeroth client security secret")
    parser.add_argument("--cookie", default="", help="raw Cookie/Set-Cookie header to import into state")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Recovered Kwai web API signing client")
    add_common(parser)
    subparsers = parser.add_subparsers(dest="action")

    sign = subparsers.add_parser("sign", help="sign any API request")
    sign.add_argument("method", help="HTTP method, e.g. GET/POST")
    sign.add_argument("path", help="API path or full URL, e.g. /rest/n/feed/hot")
    sign.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
    sign.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
    sign.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    sign.add_argument("--param-profile", default="auto", choices=PARAM_PROFILES)
    sign.add_argument("--no-rewrite", action="store_true", help="do not rewrite /rest/n to /rest/antman")
    sign.add_argument("--send", action="store_true", help="actually send request with requests")

    login = subparsers.add_parser("login", help="send a signed login request and capture token fields")
    login.add_argument("path", help="login API path or full URL")
    login.add_argument("--method", default="POST")
    login.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
    login.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
    login.add_argument("--include-auth", action="store_true")
    login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
    login.add_argument("--no-rewrite", action="store_true", help="do not rewrite /rest/n to /rest/antman")

    fetch_egid = subparsers.add_parser("fetch-egid", help="request server-issued egid using recovered DFP form")
    sign_modes = ["sha256", "double-sha256", "10418", "10417", "10405", "10418-sha256", "none"]
    fetch_egid.add_argument("--sign-mode", default="10418", choices=sign_modes)
    fetch_egid.add_argument("--plain", action="store_true", help="send unencrypted fallback sv=3 payload")
    fetch_did = subparsers.add_parser("fetch-did", help="request server-issued cloud did using recovered DFP form")
    fetch_did.add_argument("--sign-mode", default="10418", choices=sign_modes)
    fetch_did.add_argument("--plain", action="store_true", help="send unencrypted fallback sv=3 payload")

    startup = subparsers.add_parser("startup", help="POST /rest/system/startup and capture issued fields")
    startup.add_argument("--ext-id", default="")
    startup.add_argument("--gp-referer", default="")
    startup.add_argument("--oaid", default="")

    subparsers.add_parser("version-check", help="GET /rest/antman/versionCheck")

    mobile_check = subparsers.add_parser("mobile-check", help="POST n/user/mobile/checker")
    mobile_check.add_argument("--mobile", required=True)
    mobile_check.add_argument("--country-code", default="86")

    request_code = subparsers.add_parser("request-mobile-code", help="POST n/user/requestMobileCode; sends an SMS")
    request_code.add_argument("--mobile", required=True)
    request_code.add_argument("--country-code", default="86")
    request_code.add_argument("--type", type=int, default=27)

    captcha_page = subparsers.add_parser("captcha-page", help="build a manual captcha page from a 705 error_url")
    captcha_page.add_argument("--error-url", default="")
    captcha_page.add_argument("--from-response", default=str(ROOT / "ctf_phone_login_response_private.json"))
    captcha_page.add_argument("--out", default=str(ROOT / "ctf_captcha_manual.html"))

    captcha_verify = subparsers.add_parser("captcha-verify", help="exchange captcha iframe ticket for captcha_token")
    captcha_verify.add_argument("--error-url", default="", help="705 error_url; used as referer and to fill key/type/uri")
    captcha_verify.add_argument("--key", default="")
    captcha_verify.add_argument("--uri", default="")
    captcha_verify.add_argument("--type", dest="challenge_type", default="")
    captcha_verify.add_argument("--ticket", required=True)

    verify_code = subparsers.add_parser("verify-code-login", help="POST n/user/login/mobileVerifyCode")
    verify_code.add_argument("--mobile", required=True)
    verify_code.add_argument("--code", required=True)
    verify_code.add_argument("--country-code", default="86")
    verify_code.add_argument("--type", type=int, default=27)
    verify_code.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    verify_code.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
    verify_code.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
    verify_code.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    verify_code.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")
    verify_code.add_argument("--include-public-key", action="store_true", help="also add publicKey/deviceName/deviceMode fields")

    phone_login = subparsers.add_parser("phone-login", help="alias for verify-code-login; captures and saves login state")
    phone_login.add_argument("--mobile", required=True)
    phone_login.add_argument("--code", required=True)
    phone_login.add_argument("--country-code", default="86")
    phone_login.add_argument("--type", type=int, default=27)
    phone_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    phone_login.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
    phone_login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
    phone_login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    phone_login.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")
    phone_login.add_argument("--include-public-key", action="store_true", help="also add publicKey/deviceName/deviceMode fields")

    register_code_login = subparsers.add_parser("register-phone-code-login", help="POST n/user/register/mobileV2 with plugin FieldMap")
    register_code_login.add_argument("--mobile", required=True)
    register_code_login.add_argument("--code", required=True)
    register_code_login.add_argument("--country-code", default="86")
    register_code_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    register_code_login.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
    register_code_login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
    register_code_login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    register_code_login.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")

    token_login = subparsers.add_parser("token-login", help="POST n/user/login/token")
    token_login.add_argument("--login-token", required=True)
    token_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    token_login.add_argument("--no-account-keypair", action="store_true", help="do not add publicKey/raw/secret RSA fields")

    third_login = subparsers.add_parser("third-platform-login", help="POST user/thirdPlatformLogin")
    third_login.add_argument("--platform", required=True, help="e.g. wechat/qq/weibo")
    third_login.add_argument("--access-token", required=True)
    third_login.add_argument("--open-id", default="")
    third_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    third_login.add_argument("--no-account-keypair", action="store_true", help="do not add publicKey/raw/secret RSA fields")

    wechat_auth = subparsers.add_parser("wechat-auth-code", help="POST n/wechat/oauth2/authByCode")
    wechat_auth.add_argument("--code", required=True)
    wechat_auth.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    verify_trust = subparsers.add_parser("verify-trust-device", help="POST n/user/verifyTrustDevice")
    verify_trust.add_argument("--is-add-account", action="store_true")
    verify_trust.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    register_phone = subparsers.add_parser("register-phone-v2", help="POST n/user/register/mobileV2")
    register_phone.add_argument("--mobile", required=True)
    register_phone.add_argument("--code", required=True)
    register_phone.add_argument("--password", required=True)
    register_phone.add_argument("--user-name", default="")
    register_phone.add_argument("--gender", default="U")
    register_phone.add_argument("--country-code", default="86")
    register_phone.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    refresh_token = subparsers.add_parser("refresh-token", help="POST n/token/infra/refreshToken")
    refresh_token.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    subparsers.add_parser("logout", help="POST n/user/logout")

    rest_list = subparsers.add_parser("rest-list", help="list recovered REST endpoints")
    rest_list.add_argument("--safe-only", action="store_true")
    rest_list.add_argument("--info-only", action="store_true", help="only list read-only information endpoints")
    rest_list.add_argument("--category", choices=["all", *sorted(REST_INFO_CATEGORIES)], default="all", help="filter read-only information endpoints by category")

    rest_call = subparsers.add_parser("rest-call", help="call a recovered REST endpoint by name")
    rest_call.add_argument("name", choices=sorted(REST_ENDPOINTS))
    rest_call.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
    rest_call.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
    rest_call.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
    rest_call.add_argument("--print-only", action="store_true", help="only print the signed request")

    rest_smoke = subparsers.add_parser("rest-smoke", help="call safe recovered REST endpoints")
    rest_smoke.add_argument("--name", action="append", default=[], choices=sorted(REST_ENDPOINTS), help="safe endpoint name; can repeat")

    rest_info = subparsers.add_parser("rest-info-smoke", help="call read-only information REST chain")
    rest_info.add_argument("--category", choices=["all", *sorted(REST_INFO_CATEGORIES)], default="all", help="which information category to smoke test")
    rest_info.add_argument("--count", default="1", help="feed/comment count for chained probes")
    rest_info.add_argument("--keyword", default="猫", help="search keyword for search probes; empty to skip")

    private_chat_list = subparsers.add_parser("private-chat-list", help="list private chat dialogs")
    private_chat_list.add_argument("--count", default="20")
    private_chat_list.add_argument("--page", default="")
    private_chat_list.add_argument("--pcursor", default="")
    private_chat_list.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    private_chat_messages = subparsers.add_parser("private-chat-messages", help="list private chat messages with a user")
    private_chat_messages.add_argument("--user-id", required=True)
    private_chat_messages.add_argument("--page", default="")
    private_chat_messages.add_argument("--order", default="desc")
    private_chat_messages.add_argument("--pcursor", default="")
    private_chat_messages.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")

    private_chat_send = subparsers.add_parser("private-chat-send", help="send a private chat message")
    private_chat_send.add_argument("--user-id", required=True)
    private_chat_send.add_argument("--content", required=True)
    private_chat_send.add_argument("--copy", action="store_true")
    private_chat_send.add_argument("--photo-id", default="")
    private_chat_send.add_argument("--live-stream-id", default="")
    private_chat_send.add_argument("--informed-user-id", default="")
    private_chat_send.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
    private_chat_send.add_argument("--dry-run", action="store_true", help="print signed request without sending")

    capture_login = subparsers.add_parser("capture-login", help="import login state from captured request/response data")
    capture_login.add_argument("--file", default="", help="capture file: HAR/JSON/curl/raw headers/raw body")
    capture_login.add_argument("--stdin", action="store_true", help="read capture data from stdin")
    capture_login.add_argument("--no-overwrite", action="store_true", help="keep existing state fields when already set")
    capture_login.add_argument("--show-sensitive", action="store_true", help="print unredacted imported state summary")

    video_chain = subparsers.add_parser("video-chain", help="fetch hot feed, then photo info and comments for the first photo")
    video_chain.add_argument("--count", default="1")
    video_chain.add_argument("--photo-id", default="", help="skip feed extraction and use this photo id")
    video_chain.add_argument("--user-id", default="", help="preUserId for photo info when --photo-id is used")
    video_chain.add_argument("--exp-tag", default="", help="preExpTag for photo info when --photo-id is used")
    video_chain.add_argument("--llsid", default="", help="preLLSId/list-load-sequence id for photo info")
    video_chain.add_argument("--comment-count", default="1")

    subparsers.add_parser("state", help="print current device/auth state")
    return parser


def send_and_print(client: KwaiClient, signed: SignedRequest) -> int:
    response = client.send(signed)
    sys.stdout.buffer.write(f"{response.status_code}\n".encode("ascii"))
    sys.stdout.buffer.write(response.text.encode("utf-8", errors="replace"))
    sys.stdout.buffer.write(b"\n")
    return 0


def print_response(response: object) -> int:
    status_code = getattr(response, "status_code", "")
    sys.stdout.buffer.write(f"{status_code}\n".encode("utf-8", errors="replace"))
    text = getattr(response, "text", "")
    sys.stdout.buffer.write(str(text).encode("utf-8", errors="replace"))
    sys.stdout.buffer.write(b"\n")
    return 0


def print_json(payload: object) -> int:
    sys.stdout.buffer.write(json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8", errors="replace"))
    sys.stdout.buffer.write(b"\n")
    return 0


def load_captcha_error_url(error_url: str, response_path: str) -> str:
    if error_url:
        return error_url
    path = Path(response_path)
    if path.exists():
        payload = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(payload, dict):
            return str(payload.get("error_url") or "")
    return ""


def parse_extra_args(args: argparse.Namespace) -> dict[str, str]:
    extra = parse_pairs(getattr(args, "extra", []))
    captcha_token = getattr(args, "captcha_token", "")
    if captcha_token:
        extra["captcha_token"] = captcha_token
    return extra


def load_capture_payload(args: argparse.Namespace) -> object:
    if getattr(args, "stdin", False):
        return sys.stdin.buffer.read().decode("utf-8", errors="replace")
    capture_file = getattr(args, "file", "")
    if not capture_file:
        raise SystemExit("capture-login requires --file or --stdin")
    text = Path(capture_file).read_bytes().decode("utf-8-sig", errors="replace")
    try:
        return json.loads(text)
    except Exception:
        return text


def main(argv: list[str] | None = None) -> int:
    raw = list(sys.argv[1:] if argv is None else argv)
    if raw and raw[0].upper() in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
        raw.insert(0, "sign")
    args = build_parser().parse_args(raw)
    if not args.action:
        args.action = "state"
    device = build_device(args)
    client = KwaiClient(device, host=args.host, assets_root=args.assets)

    try:
        if args.action == "state":
            print(json.dumps(device.to_dict(), ensure_ascii=False, indent=2))
            return 0

        if args.action == "captcha-page":
            error_url = load_captcha_error_url(args.error_url, args.from_response)
            if not error_url:
                raise SystemExit("captcha-page requires --error-url or a response JSON containing error_url")
            challenge, response = client.fetch_captcha_challenge(error_url)
            latest_page = ROOT / "ctf_captcha_page_latest.html"
            latest_page.write_text(getattr(response, "text", "") or "", encoding="utf-8")
            fallback_page = ROOT / "ctf_captcha_page.html"
            used_fallback = False
            if not challenge.captcha_session and fallback_page.exists():
                fallback = parse_captcha_challenge(error_url, fallback_page.read_text(encoding="utf-8", errors="replace"))
                if fallback.captcha_session:
                    challenge = fallback
                    used_fallback = True
            out = Path(args.out)
            out.write_text(build_captcha_manual_html(challenge), encoding="utf-8")
            return print_json(
                {
                    "out": str(out),
                    "page_status": getattr(response, "status_code", None),
                    "saved_page": str(latest_page),
                    "used_saved_page_fallback": used_fallback,
                    "key": challenge.key,
                    "type": challenge.verify_type,
                    "uri": challenge.uri,
                    "captcha_session_found": bool(challenge.captcha_session),
                    "iframe_url": challenge.iframe_url,
                }
            )

        if args.action == "captcha-verify":
            challenge = parse_captcha_challenge(args.error_url) if args.error_url else None
            if args.error_url:
                try:
                    challenge, _ = client.fetch_captcha_challenge(args.error_url)
                except Exception:
                    challenge = parse_captcha_challenge(args.error_url)
            key = args.key or (challenge.key if challenge else "")
            uri = args.uri or (challenge.uri if challenge else "") or "/rest/n/user/login/mobileVerifyCode"
            challenge_type = args.challenge_type or (challenge.verify_type if challenge else "") or "7"
            if not key:
                raise SystemExit("captcha-verify requires --key or --error-url with key")
            return print_response(
                client.captcha_verify(
                    key,
                    args.ticket,
                    uri=uri,
                    verify_type=challenge_type,
                    referer=args.error_url,
                )
            )

        if args.action == "rest-list":
            endpoints = []
            names = client.endpoint_names(safe_only=args.safe_only)
            if args.info_only:
                info_names = INFO_REST_ENDPOINT_NAMES if args.category == "all" else REST_INFO_CATEGORIES[args.category]
                names = [name for name in names if name in info_names]
            for name in names:
                endpoint = REST_ENDPOINTS[name]
                endpoints.append(
                    {
                        "name": endpoint.name,
                        "method": endpoint.method,
                        "path": endpoint.path,
                        "safe": endpoint.safe,
                        "include_auth": endpoint.include_auth,
                        "service": endpoint.service,
                        "java_method": endpoint.java_method,
                        "fields": list(endpoint.fields),
                        "query_fields": list(endpoint.query_fields),
                        "param_profile": endpoint.param_profile,
                        "rewrite_path": endpoint.rewrite_path,
                        "defaults": dict(endpoint.defaults),
                        "query_defaults": dict(endpoint.query_defaults),
                        "description": endpoint.description,
                    }
                )
            return print_json(endpoints)

        if args.action == "rest-call":
            signed = client.rest_request(
                args.name,
                params=parse_pairs(args.param),
                data=parse_pairs(args.data),
                sig3_mode=args.sig3_mode,
            )
            if args.print_only:
                print_signed(signed)
                return 0
            return send_and_print(client, signed)

        if args.action == "rest-smoke":
            names = args.name or None
            results = client.smoke_test_rest(names)
            return print_json(results)

        if args.action == "rest-info-smoke":
            return print_json(client.smoke_test_info_rest(category=args.category, count=args.count, keyword=args.keyword))

        if args.action == "private-chat-list":
            return print_response(
                client.private_chat_list(
                    count=args.count,
                    page=args.page,
                    pcursor=args.pcursor,
                    extra=parse_extra_args(args),
                )
            )

        if args.action == "private-chat-messages":
            return print_response(
                client.private_chat_messages(
                    args.user_id,
                    page=args.page,
                    order=args.order,
                    pcursor=args.pcursor,
                    extra=parse_extra_args(args),
                )
            )

        if args.action == "private-chat-send":
            result = client.private_chat_send(
                args.user_id,
                args.content,
                copy=args.copy,
                photo_id=args.photo_id,
                live_stream_id=args.live_stream_id,
                informed_user_id=args.informed_user_id,
                extra=parse_extra_args(args),
                send=not args.dry_run,
            )
            if args.dry_run:
                print_signed(result)
                return 0
            return print_response(result)

        if args.action == "capture-login":
            changed = device.import_login_capture(load_capture_payload(args), overwrite=not args.no_overwrite)
            state = device.to_dict() if args.show_sensitive else device.to_redacted_dict()
            return print_json(
                {
                    "changed_fields": sorted(changed),
                    "has_login": bool(device.token or device.api_st or device.service_token),
                    "state": state,
                }
            )

        if args.action == "video-chain":
            result: dict[str, object] = {}
            photo_id = args.photo_id
            user_id = args.user_id
            exp_tag = args.exp_tag
            llsid = args.llsid
            photo_index = 0
            if not photo_id:
                feed_response = client.video_feed_hot(count=args.count)
                feed_payload = feed_response.json()
                result["feed_status"] = feed_response.status_code
                result["feed_result"] = feed_payload.get("result") if isinstance(feed_payload, dict) else None
                result["feed_preview"] = getattr(feed_response, "text", "")[:500]
                extracted = client.extract_video_ids(feed_payload)
                result["extracted"] = extracted[:5]
                if extracted:
                    photo_id = extracted[0]["photo_id"]
                    user_id = extracted[0].get("user_id", "")
                    exp_tag = extracted[0].get("exp_tag", "")
                    llsid = extracted[0].get("llsid", "")
                    result["user_id"] = user_id
                    result["exp_tag"] = exp_tag
                    result["llsid"] = llsid
            result["photo_id"] = photo_id
            if photo_id:
                info_response = client.video_photo_info(
                    photo_id,
                    user_id=user_id,
                    exp_tag=exp_tag,
                    llsid=llsid,
                    photo_index=photo_index,
                )
                comments_response = client.video_photo_comments(photo_id, count=args.comment_count)
                result["photo_info_status"] = info_response.status_code
                result["photo_info_preview"] = getattr(info_response, "text", "")[:1200]
                result["comments_status"] = comments_response.status_code
                result["comments_preview"] = getattr(comments_response, "text", "")[:1200]
            return print_json(result)

        if args.action == "sign":
            params = parse_pairs(args.param)
            data = parse_pairs(args.data)
            signed = client.signed_request(
                args.method,
                args.path,
                params=params,
                data=data if args.method.upper() != "GET" else None,
                sign_body=args.method.upper() != "GET",
                sig3_mode=args.sig3_mode,
                param_profile=args.param_profile,
                rewrite_path=not args.no_rewrite,
            )
            if args.send:
                return send_and_print(client, signed)
            print_signed(signed)
            return 0

        if args.action == "login":
            response = client.login(
                args.path,
                data=parse_pairs(args.data),
                params=parse_pairs(args.param),
                method=args.method,
                include_auth=args.include_auth,
                sig3_mode=args.sig3_mode,
                param_profile=args.param_profile,
                rewrite_path=not args.no_rewrite,
            )
            return print_response(response)

        if args.action == "startup":
            return print_response(client.startup(ext_id=args.ext_id, gp_referer=args.gp_referer, oaid=args.oaid))

        if args.action == "version-check":
            return print_response(client.version_check())

        if args.action == "mobile-check":
            return print_response(client.login_mobile_check(args.mobile, country_code=args.country_code))

        if args.action == "request-mobile-code":
            return print_response(client.request_mobile_code(args.mobile, country_code=args.country_code, code_type=args.type))

        if args.action == "verify-code-login":
            return print_response(
                client.verify_code_login(
                    args.mobile,
                    args.code,
                    country_code=args.country_code,
                    code_type=args.type,
                    extra=parse_extra_args(args),
                    param_profile=args.param_profile,
                    sig3_mode=args.sig3_mode,
                    use_account_keypair=not args.no_account_keypair,
                    include_public_key=args.include_public_key,
                )
            )

        if args.action == "phone-login":
            return print_response(
                client.phone_code_login(
                    args.mobile,
                    args.code,
                    country_code=args.country_code,
                    code_type=args.type,
                    extra=parse_extra_args(args),
                    param_profile=args.param_profile,
                    sig3_mode=args.sig3_mode,
                    use_account_keypair=not args.no_account_keypair,
                    include_public_key=args.include_public_key,
                )
            )

        if args.action == "register-phone-code-login":
            return print_response(
                client.register_by_phone_code_login(
                    args.mobile,
                    args.code,
                    country_code=args.country_code,
                    extra=parse_extra_args(args),
                    param_profile=args.param_profile,
                    sig3_mode=args.sig3_mode,
                    use_account_keypair=not args.no_account_keypair,
                )
            )

        if args.action == "token-login":
            return print_response(
                client.token_login(
                    args.login_token,
                    extra=parse_extra_args(args),
                    use_account_keypair=not args.no_account_keypair,
                )
            )

        if args.action == "third-platform-login":
            return print_response(
                client.third_platform_login(
                    args.platform,
                    args.access_token,
                    open_id=args.open_id,
                    extra=parse_extra_args(args),
                    use_account_keypair=not args.no_account_keypair,
                )
            )

        if args.action == "wechat-auth-code":
            return print_response(client.auth_wechat_code(args.code, extra=parse_extra_args(args)))

        if args.action == "verify-trust-device":
            return print_response(
                client.verify_trust_device(
                    is_add_account=args.is_add_account,
                    extra=parse_extra_args(args),
                )
            )

        if args.action == "register-phone-v2":
            return print_response(
                client.register_by_phone_v2(
                    args.mobile,
                    args.code,
                    args.password,
                    user_name=args.user_name,
                    gender=args.gender,
                    country_code=args.country_code,
                    extra=parse_extra_args(args),
                )
            )

        if args.action == "refresh-token":
            return print_response(client.refresh_token(extra=parse_extra_args(args)))

        if args.action == "logout":
            return print_response(client.logout())

        if args.action == "fetch-egid":
            response = client.fetch_egid(sign_mode=args.sign_mode, encrypt_payload=not args.plain)
            print(response.status_code)
            print(response.text)
            return 0

        if args.action == "fetch-did":
            response = client.fetch_did(sign_mode=args.sign_mode, encrypt_payload=not args.plain)
            print(response.status_code)
            print(response.text)
            return 0

        raise SystemExit(f"unknown action: {args.action}")
    finally:
        maybe_save(device, args)


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))

0x0A 抓包环境:Cronet-only QUIC 降级脚本

之前实机 hook ClassLoader 崩过,说明太激进的 Java/ART 热路径 hook 会触发不稳定甚至检测。因此最终 Frida 脚本只处理 Cronet Builder,不枚举 ClassLoader,不 hook UDP/native。

Frida 完整脚本:cronet_disable_quic.js

  • 文件:frida/cronet_disable_quic.js
  • 行数:244
  • 字节:6952
  • SHA256:4e3cc86b7d73676b3e4ff0e8ab61afc86021d41d750229a384e1fbd0f35f03ad

为什么看它:它只尝试禁用 Cronet QUIC/HTTP3,让请求回落到 TCP/TLS,方便代理抓包。

读完得到的结论:这不是绕过 HTTPS,也不是绕风控;只是降低 QUIC 对抓包的影响。

'use strict';

/*
 * com.smile.gifmaker / Kwai 14.0.40.46379
 * Cronet-only QUIC downgrade helper.
 *
 * Low-intrusion build for anti-crash/anti-detection cases:
 * - no ClassLoader.loadClass hook
 * - no enumerateLoadedClasses / enumerateClassLoaders
 * - no OkHttp, UDP, socket, or native hooks
 * - only patches visible Cronet Java builder APIs
 */

const CFG = {
  packageName: 'com.smile.gifmaker',
  versionName: '14.0.40.46379',
  verbose: true,
  scanIntervalMs: 2000,
  stopAfterMs: 120000,
};

const CRONET_BUILDERS = [
  'org.chromium.net.CronetEngine$Builder',
  'org.chromium.net.ExperimentalCronetEngine$Builder',
  'org.chromium.net.impl.CronetEngineBuilderImpl',
  'org.chromium.net.impl.ExperimentalCronetEngineBuilderImpl',
  'org.chromium.net.impl.NativeCronetEngineBuilderImpl',
  'org.chromium.net.impl.JavaCronetEngineBuilderImpl',
  'com.google.android.gms.net.CronetEngine$Builder',
];

const installed = Object.create(null);
const startTime = Date.now();
let timer = null;

function log(msg) {
  if (CFG.verbose) {
    console.log('[kws-cronet-noquic] ' + msg);
  }
}

function once(key, fn) {
  if (installed[key]) return false;
  try {
    fn();
    installed[key] = true;
    log('installed ' + key);
    return true;
  } catch (e) {
    const s = String(e);
    if (s.indexOf('ClassNotFoundException') === -1 && s.indexOf('Unable to find class') === -1) {
      log('skip ' + key + ': ' + s);
    }
    return false;
  }
}

function noQuicOptions(input) {
  let obj = {};
  const text = input === null || input === undefined ? '' : String(input).trim();
  if (text.length > 0) {
    try {
      obj = JSON.parse(text);
    } catch (_) {
      obj = {};
    }
  }
  if (!obj.QUIC || typeof obj.QUIC !== 'object' || Array.isArray(obj.QUIC)) {
    obj.QUIC = {};
  }
  obj.QUIC.enable_quic = false;
  obj.QUIC.enabled = false;
  obj.QUIC.host_whitelist = '';
  obj.QUIC.quic_host_whitelist = '';
  obj.QUIC.connection_options = '';
  obj.QUIC.client_connection_options = '';
  obj.QUIC.max_server_configs_stored_in_properties = 0;
  return JSON.stringify(obj);
}

function hookBuilder(className) {
  const Builder = Java.use(className);
  let changed = false;

  if (Builder.enableQuic) {
    Builder.enableQuic.overloads.forEach(function (ov, i) {
      if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'boolean') {
        changed = once(className + '.enableQuic#' + i, function () {
          ov.implementation = function (enabled) {
            log(className + '.enableQuic(' + enabled + ') => false');
            return ov.call(this, false);
          };
        }) || changed;
      }
    });
  }

  if (Builder.setQuicEnabled) {
    Builder.setQuicEnabled.overloads.forEach(function (ov, i) {
      if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'boolean') {
        changed = once(className + '.setQuicEnabled#' + i, function () {
          ov.implementation = function (enabled) {
            log(className + '.setQuicEnabled(' + enabled + ') => false');
            return ov.call(this, false);
          };
        }) || changed;
      }
    });
  }

  if (Builder.addQuicHint) {
    Builder.addQuicHint.overloads.forEach(function (ov, i) {
      changed = once(className + '.addQuicHint#' + i, function () {
        ov.implementation = function () {
          log(className + '.addQuicHint(...) ignored');
          return this;
        };
      }) || changed;
    });
  }

  if (Builder.setExperimentalOptions) {
    Builder.setExperimentalOptions.overloads.forEach(function (ov, i) {
      if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'java.lang.String') {
        changed = once(className + '.setExperimentalOptions#' + i, function () {
          ov.implementation = function (options) {
            log(className + '.setExperimentalOptions(...) => QUIC off');
            return ov.call(this, noQuicOptions(options));
          };
        }) || changed;
      }
    });
  }

  if (Builder.build) {
    Builder.build.overloads.forEach(function (ov, i) {
      changed = once(className + '.build#' + i, function () {
        ov.implementation = function () {
          try {
            if (this.enableQuic) this.enableQuic(false);
          } catch (_) {}
          try {
            if (this.setQuicEnabled) this.setQuicEnabled(false);
          } catch (_) {}
          try {
            if (this.setExperimentalOptions) this.setExperimentalOptions(noQuicOptions('{}'));
          } catch (_) {}
          log(className + '.build() => force QUIC off before build');
          return ov.call(this);
        };
      }) || changed;
    });
  }

  return changed;
}

function patchKnownCronetClasses() {
  let count = 0;
  CRONET_BUILDERS.forEach(function (name) {
    try {
      if (hookBuilder(name)) count++;
    } catch (e) {
      const s = String(e);
      if (s.indexOf('ClassNotFoundException') === -1 && s.indexOf('Unable to find class') === -1) {
        log('probe failed ' + name + ': ' + s);
      }
    }
  });
  return count;
}

function withLoader(loader, label) {
  if (!loader) return 0;
  let previous = null;
  try {
    previous = Java.classFactory.loader;
    Java.classFactory.loader = loader;
    const hits = patchKnownCronetClasses();
    if (hits > 0) log(label + ' loader hits=' + hits);
    return hits;
  } catch (e) {
    log(label + ' loader probe failed: ' + e);
    return 0;
  } finally {
    try {
      Java.classFactory.loader = previous;
    } catch (_) {}
  }
}

function getApplication() {
  try {
    const ActivityThread = Java.use('android.app.ActivityThread');
    return ActivityThread.currentApplication();
  } catch (_) {
    return null;
  }
}

function identityAndLoaders() {
  const loaders = [];
  try {
    const app = getApplication();
    if (app) {
      const pkg = String(app.getPackageName());
      log('attached package=' + pkg + ', target=' + CFG.packageName + ', apk=' + CFG.versionName);
      loaders.push({ label: 'application', loader: app.getClassLoader() });
    }
  } catch (e) {
    log('application loader skipped: ' + e);
  }
  try {
    const Thread = Java.use('java.lang.Thread');
    loaders.push({ label: 'context', loader: Thread.currentThread().getContextClassLoader() });
  } catch (e) {
    log('context loader skipped: ' + e);
  }
  return loaders;
}

function scan() {
  Java.perform(function () {
    let hits = patchKnownCronetClasses();
    identityAndLoaders().forEach(function (item) {
      hits += withLoader(item.loader, item.label);
    });
    if (hits > 0) {
      log('Cronet hooks ready, hits=' + hits);
    }
    if (Date.now() - startTime > CFG.stopAfterMs && timer !== null) {
      clearInterval(timer);
      timer = null;
      log('stop polling; keep installed hooks active');
    }
  });
}

if (Java.available) {
  setImmediate(scan);
  timer = setInterval(scan, CFG.scanIntervalMs);
} else {
  log('Java runtime unavailable');
}

0x0B 测试:把逆向结论固定下来

逆向最怕“今天能跑,明天改坏”。所以所有关键结论都要进测试:sig shape、sig3 三模式、DFP form、Azeroth canonical、REST endpoint shape、HAR 导入、私信 dry-run。

测试完整代码:tests/test_recovered_algorithms.py

  • 文件:tests/test_recovered_algorithms.py
  • 行数:854
  • 字节:35194
  • SHA256:d82a39ef7233e191546223a140951681813863fed300e2a4c53125a045a259fa

为什么看它:它固定算法、协议 shape、HAR 导入策略、私信 API shape。

读完得到的结论:测试是本文证据链的最后一环:不只是分析出来,还要能被机器重复验证。

from pathlib import Path

from kws_recovered.algorithms import (
    APPKEY_AZEROTH,
    APPKEY_MAIN,
    build_lite_dfp,
    decode_zt_png,
    find_zt_table,
    get_clock,
    load_kwai_dynamic_sections,
    kwai_sig,
    kwai_token_sig,
    native_obfuscated_string,
    azeroth_canonical,
    azeroth_nonce,
    rc4_ksa_prga,
    sig3_10405,
    sig3_10417,
    sig3_10418,
)
from kws_recovered.client import (
    INFO_REST_ENDPOINT_NAMES,
    MAIN_VIDEO_LOGIN_APPVER,
    MAIN_VIDEO_LOGIN_VER,
    REST_ENDPOINTS,
    REST_INFO_CATEGORIES,
    KwaiClient,
    KwaiDeviceContext,
)


ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "resources" / "assets"


class FakeResponse:
    status_code = 200
    text = '{"result":1}'
    headers = {}
    cookies = {}

    def json(self):
        return {"result": 1}


class FakeSession:
    def __init__(self):
        self.calls = []

    def request(self, method, url, **kwargs):
        self.calls.append((method, url, kwargs))
        return FakeResponse()


class RoutedFakeSession:
    def __init__(self):
        self.calls = []

    def request(self, method, url, **kwargs):
        self.calls.append((method, url, kwargs))
        response = FakeResponse()
        response.text = '{"result":1,"url":"' + url + '"}'
        return response


def test_decode_known_zt_tables():
    known = {
        APPKEY_MAIN: ("2", "gAZFlwExV0NN2XzT"),
        APPKEY_AZEROTH: ("1", "LqARcOYEg8gGHby2"),
        "0d2aa853-8455-41b5-b06c-9a36680ef3f0": ("1", "POLKM52lbfYgwD2g"),
        "0dc6922f-e3ee-4c57-8eb1-b72d86635497": ("1", "h0hF5ZqvBZagMGhn"),
        "5bbcf3cd-727b-48ab-b4b4-5f01e61ee9a5": ("1", "lealm6bxeMABH3rQ"),
        "bbd910da-fda5-49e7-8667-f57200dac474": ("1", "lpYKvL0Mz9bEtHXO"),
    }
    for appkey, (version, x5) in known.items():
        table = decode_zt_png(ASSETS / "saio_res" / f"zt_{appkey}.png")
        assert table.appkey == appkey
        assert str(table.version) == version
        assert table.platform == "Android"
        assert table.x5 == x5
        assert table.selector_byte == ord("-")


def test_decode_kwai_dynamic_zt_container():
    sections = load_kwai_dynamic_sections(ASSETS / "video_yh_loading_icon.png")
    assert len(sections["TyiBoxes_147456"]) == 147456
    assert len(sections["TBoxes_40960"]) == 40960
    assert len(sections["TyiTables_4096"]) == 4096
    assert len(sections["sha256key00_64"]) == 64
    assert len(sections["sha256key01_64"]) == 64
    assert sections["custom_2018.png"].startswith(b"\x89PNG\r\n\x1a\n")


def test_native_obfuscated_string_decoder():
    assert native_obfuscated_string(bytes.fromhex("0e7c506372fb379527383058633608")) == "sha256key00_64"
    assert native_obfuscated_string(bytes.fromhex("0f5b416b02a1799b311e315c0b3409c4")) == "TyiBoxes_147456"


def test_core_hash_algorithms_are_stable():
    assert get_clock(b"") == "d9fb5ffc70b7624a88d77c04c43f8e26"
    assert kwai_sig({"b": "2", "a": "1"}) == get_clock(b"a=1b=2")
    assert kwai_token_sig("a" * 32, "salt") == "e07c9dd4d2addfecb5f0f5218ce70e23c226a0dc11c31e5e43a88f346aa93af1"


def test_old_sig3_10405_shape_and_fixed_value():
    assert sig3_10405(b"abc", now=1700000000) == "3097492719e0ba7816bf8f01cfea414140de5dae22"


def test_new_sig3_10417_plain_and_wrapped():
    table = decode_zt_png(ASSETS / "saio_res" / f"zt_{APPKEY_MAIN}.png")
    plain = sig3_10417(b"abc", None, now=1700000000, counter=1, runtime_value=0, feature_bits=0, wrap=False)
    wrapped = sig3_10417(b"abc", table, now=1700000000, counter=1, runtime_value=0, feature_bits=0)
    assert plain == "1809595a5d5c5f5e505053529715736349b9182f4d414f59"
    assert len(plain) == 48
    assert wrapped == "5a54edcdb18f015d083f5b6b465c6d5171176a6bd23a7e40598f351e56417d56"
    assert len(wrapped) == 64


def test_default_atlas_sign_10418_shape_and_fixed_value():
    table = find_zt_table(APPKEY_MAIN, ASSETS)
    sig3 = sig3_10418(b"abc", table)
    assert sig3 == "c97ab3636f06fdae22e53a155e3ba66db65f40af9942a070084bdc4fd0264287606fff704c33f0439d2f342ed83c930d"
    assert len(sig3) == 96


def test_dfp_and_rc4_helpers():
    dfp = build_lite_dfp({"k5": "demo"})
    assert "%3D" in dfp or dfp
    data = b"hello world"
    encrypted = rc4_ksa_prga(b"key", data)
    assert rc4_ksa_prga(b"key", encrypted) == data


def test_client_builds_signed_any_web_api_request():
    device = KwaiDeviceContext(did="did-demo", egid="egid-demo", rdid="rdid-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
    signed = client.signed_request("GET", "/rest/n/feed/hot", params={"count": 1})
    assert signed.url == "https://apissl.ksapisrv.com/rest/antman/feed/hot"
    assert signed.method == "GET"
    assert signed.params["sig"]
    assert len(signed.params["sig"]) == 32
    assert len(signed.params["__NS_sig3"]) == 96
    assert signed.params["kpn"] == "KUAISHOU_ANTMAN"
    assert signed.params["appver"] == "10.3.39.6655"
    assert signed.params["newOc"] == "ANTMAN_PC"


def test_client_salt_is_used_but_not_sent():
    device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", client_salt="salt-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
    signed = client.signed_request("GET", "/rest/n/feed/hot", params={"count": 1, "client_salt": "request-salt"})
    assert "client_salt" not in signed.params
    assert signed.params["token"] == "token-demo"
    assert signed.params["kuaishou.api_st"] == "api-st-demo"
    assert signed.params["__NStokensig"] == kwai_token_sig(signed.params["sig"], "request-salt")


def test_client_form_request_matches_interceptor_split():
    device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", client_salt="salt-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
    signed = client.signed_request("POST", "/rest/n/account/login", params={"q": "1"}, data={"mobile": "13800000000"})
    assert "lat" in signed.params
    assert signed.params["q"] == "1"
    assert "sig" not in signed.params
    assert isinstance(signed.data, dict)
    assert signed.data["mobile"] == "13800000000"
    assert signed.data["token"] == "token-demo"
    assert signed.data["kuaishou.api_st"] == "api-st-demo"
    assert signed.data["sig"]
    assert "sig2" not in signed.data
    assert signed.data["__NStokensig"] == kwai_token_sig(signed.data["sig"], "salt-demo")


def test_login_helpers_use_recovered_plugin_endpoints():
    session = FakeSession()
    device = KwaiDeviceContext(did="did-demo", rdid="rdid-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com", session=session)

    client.login_mobile_check("13800000000")
    client.request_mobile_code("13800000000", code_type=1)
    client.verify_code_login("13800000000", "123456", extra={"captcha_token": "captcha-demo"})

    urls = [call[1] for call in session.calls]
    assert urls == [
        "https://apissl.ksapisrv.com/rest/n/user/mobile/checker",
        "https://apissl.ksapisrv.com/rest/n/user/requestMobileCode",
        "https://apissl.ksapisrv.com/rest/n/user/login/mobileVerifyCode",
    ]
    for _, _, kwargs in session.calls:
        assert kwargs["params"]["did"].startswith("ANDROID_")
        assert kwargs["params"]["kpn"] == "KUAISHOU"
        assert kwargs["params"]["appver"] == MAIN_VIDEO_LOGIN_APPVER
        assert kwargs["params"]["ver"] == MAIN_VIDEO_LOGIN_VER
        assert "sig" not in kwargs["params"]
        assert kwargs["data"]["sig"]
        assert len(kwargs["data"]["__NS_sig3"]) == 96
    assert session.calls[2][2]["data"]["code"] == "123456"
    assert session.calls[2][2]["data"]["type"] == "27"
    assert session.calls[2][2]["data"]["captcha_token"] == "captcha-demo"
    assert session.calls[2][2]["data"]["raw"]
    assert session.calls[2][2]["data"]["secret"]


def test_auto_profile_keeps_login_on_working_legacy_video_signature():
    device = KwaiDeviceContext(did="did-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")

    signed = client.signed_request(
        "POST",
        "/rest/n/user/mobile/checker",
        data={"mobileCountryCode": "86", "mobile": "13800000000"},
        include_auth=False,
        param_profile="auto",
    )

    assert signed.url == "https://apissl.ksapisrv.com/rest/n/user/mobile/checker"
    assert signed.params["kpn"] == "KUAISHOU"
    assert signed.params["appver"] == MAIN_VIDEO_LOGIN_APPVER
    assert signed.params["ver"] == MAIN_VIDEO_LOGIN_VER
    assert "isAntman" not in signed.params
    assert isinstance(signed.data, dict)
    assert signed.data["sig"]
    assert len(signed.data["__NS_sig3"]) == 96


def test_azeroth_request_uses_14x_params_and_sig3_body():
    device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", ud="123")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://api.kuaishouzt.com")

    signed = client.signed_request(
        "POST",
        "/rest/n/user/login/mobileVerifyCode",
        data={"mobile": "13800000000", "code": "000000"},
        include_auth=False,
        param_profile="azeroth",
        rewrite_path=False,
    )

    assert signed.url == "https://api.kuaishouzt.com/rest/n/user/login/mobileVerifyCode"
    assert signed.params["kpn"] == "KUAISHOU"
    assert signed.params["kpf"] == "ANDROID_PHONE"
    assert signed.params["appver"].startswith("14.3.30")
    assert signed.params["ver"] == "14.3"
    assert signed.params["did"].startswith("ANDROID_")
    assert signed.params["userId"] == "123"
    assert "sig" not in signed.params
    assert "sig" not in signed.data
    assert "token" not in signed.params
    assert "kuaishou.api_st" not in signed.params
    assert len(signed.data["__NS_sig3"]) == 96


def test_azeroth_nonce_matches_java_int_shift_semantics():
    assert azeroth_nonce(now_ms=1700000000000, rand32=0x01020304) == 0x01B25715
    assert azeroth_nonce(now_ms=1700000000000, rand32=0xFFFFFFFF) == -1


def test_azeroth_canonical_filters_signature_fields():
    canonical = azeroth_canonical("post", "/rest/demo", {"b": "2", "__NS_sig3": "skip", "a": None})
    assert canonical == "POST&/rest/demo&a=&b=2"


def test_register_and_token_login_forms_are_signed():
    session = FakeSession()
    client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)

    client.token_login("login-token-demo", extra={"publicKey": "public-key-demo"})
    client.register_by_phone_v2("13800000000", "123456", "pw", user_name="u", gender="M")
    client.third_platform_login("wechat", "access-token-demo", open_id="open-id-demo", extra={"unionId": "union-demo"})
    client.auth_wechat_code("wechat-code-demo")

    token_form = session.calls[0][2]["data"]
    register_form = session.calls[1][2]["data"]
    third_form = session.calls[2][2]["data"]
    wechat_form = session.calls[3][2]["data"]
    assert session.calls[0][1].endswith("/rest/antman/user/login/token")
    assert token_form["token"] == "login-token-demo"
    assert token_form["type"] == "302"
    assert token_form["publicKey"] == "public-key-demo"
    assert token_form["raw"] and token_form["secret"]
    assert token_form["sig"] and token_form["__NS_sig3"]
    assert session.calls[1][1].endswith("/rest/antman/user/register/mobileV2")
    assert register_form["mobileCode"] == "123456"
    assert register_form["password"] == "pw"
    assert register_form["gender"] == "M"
    assert session.calls[2][1].endswith("/rest/user/thirdPlatformLogin")
    assert third_form["platform"] == "wechat"
    assert third_form["accessToken"] == "access-token-demo"
    assert third_form["openId"] == "open-id-demo"
    assert third_form["unionId"] == "union-demo"
    assert third_form["publicKey"] and third_form["raw"] and third_form["secret"]
    assert session.calls[3][1].endswith("/rest/antman/wechat/oauth2/authByCode")
    assert wechat_form["code"] == "wechat-code-demo"


def test_all_recovered_rest_endpoints_can_build_signed_requests():
    client = KwaiClient(
        KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
        assets_root=str(ASSETS),
        session=FakeSession(),
    )
    failures = []
    for name, endpoint in sorted(REST_ENDPOINTS.items()):
        data = {field: f"{field}-demo" for field in endpoint.fields}
        params = {field: f"{field}-demo" for field in endpoint.query_fields}
        if endpoint.path == "@Url":
            data["url"] = "https://example.test/probe"
        try:
            signed = client.rest_request(name, data=data, params=params)
        except Exception as exc:
            failures.append(f"{name}: {exc}")
            continue
        assert signed.method in {"GET", "POST"}
        assert signed.url.startswith("http")
        assert signed.params.get("__NS_sig3") or (isinstance(signed.data, dict) and signed.data.get("__NS_sig3"))
    assert failures == []


def test_refresh_and_logout_send_token_salt_fields():
    session = FakeSession()
    device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo", api_st="api-st-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), session=session)

    client.refresh_token(extra={"refreshToken": "refresh-demo"})
    client.logout()

    refresh_form = session.calls[0][2]["data"]
    logout_form = session.calls[1][2]["data"]
    assert session.calls[0][1].endswith("/rest/n/token/infra/refreshToken")
    assert refresh_form["refreshToken"] == "refresh-demo"
    assert refresh_form["client_salt"] == "salt-demo"
    assert refresh_form["__NStokensig"] == kwai_token_sig(refresh_form["sig"], "salt-demo")
    assert session.calls[1][1].endswith("/rest/antman/user/logout")
    assert logout_form["token"] == "token-demo"
    assert logout_form["client_salt"] == "salt-demo"
    assert logout_form["__NStokensig"] == kwai_token_sig(logout_form["sig"], "salt-demo")


def test_rest_endpoint_catalog_builds_signed_requests():
    device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())

    assert "startup" in REST_ENDPOINTS
    assert "request_mobile_code" in REST_ENDPOINTS
    assert "startup" in client.endpoint_names(safe_only=True)
    assert "request_mobile_code" not in client.endpoint_names(safe_only=True)

    signed = client.rest_request("mobile_check", data={"mobile": "13800000000"})
    assert signed.url.endswith("/rest/n/user/mobile/checker")
    assert isinstance(signed.data, dict)
    assert signed.data["mobile"] == "13800000000"
    assert signed.data["sig"]
    assert "token" not in signed.data

    refresh = client.rest_request("refresh_token", data={"refreshToken": "refresh-demo"})
    assert refresh.url.endswith("/rest/n/token/infra/refreshToken")
    assert isinstance(refresh.data, dict)
    assert refresh.data["token"] == "token-demo"
    assert refresh.data["client_salt"] == "salt-demo"

    startup = client.rest_request("startup")
    assert startup.url.endswith("/rest/antman/system/startup")
    assert startup.params["extId"] == ""
    assert isinstance(startup.data, dict)
    assert startup.data["gp_referer"] == ""


def test_auto_retrofit_endpoint_catalog_is_stable_and_usable():
    device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())

    auto_names = [name for name, endpoint in REST_ENDPOINTS.items() if endpoint.service]
    assert len(auto_names) >= 200
    assert "api_check_version" in REST_ENDPOINTS
    assert "api_check_version_2" not in REST_ENDPOINTS
    assert "https_login_mobile_check" in REST_ENDPOINTS
    assert "pay_verify_id_card" in REST_ENDPOINTS
    assert "api_get_photo_infos" in REST_ENDPOINTS
    assert "api_comment_list_v2" in REST_ENDPOINTS
    assert "api_search" in REST_ENDPOINTS
    assert "api_get_feed_selection" in REST_ENDPOINTS
    assert "azeroth_configs" in REST_ENDPOINTS
    assert "plugin_startup" in REST_ENDPOINTS
    assert "test_speed_dynamic_url" in REST_ENDPOINTS

    mobile_endpoint = REST_ENDPOINTS["https_login_mobile_check"]
    assert mobile_endpoint.service == "KwaiHttpsService"
    assert mobile_endpoint.java_method == "loginMobileCheck"
    assert mobile_endpoint.fields == ("mobileCountryCode", "mobile")
    assert mobile_endpoint.include_auth is False

    signed = client.rest_request("https_login_mobile_check", data={"mobileCountryCode": "86", "mobile": "13800000000"})
    assert signed.url.endswith("/rest/antman/user/mobile/checker")
    assert isinstance(signed.data, dict)
    assert signed.data["mobile"] == "13800000000"
    assert signed.data["sig"]
    assert "token" not in signed.data

    selection_endpoint = REST_ENDPOINTS["api_get_feed_selection"]
    assert selection_endpoint.fields[:4] == ("page", "coldStart", "count", "pcursor")
    assert selection_endpoint.query_fields == ("cold",)

    selection = client.rest_request("api_get_feed_selection", data={"count": 3}, params={"cold": True})
    assert selection.url.endswith("/rest/antman/feed/selection")
    assert selection.params["cold"] == "true"
    assert isinstance(selection.data, dict)
    assert selection.data["count"] == "3"
    assert selection.data["page"] == ""


def test_manual_java_layer_endpoints_have_special_parameter_routing():
    client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=FakeSession())

    plugin_startup = client.rest_request("plugin_startup")
    assert plugin_startup.url.endswith("/rest/antman/system/startup")
    assert plugin_startup.params["extId"] == ""
    assert isinstance(plugin_startup.data, dict)
    assert plugin_startup.data["gp_referer"] == ""
    assert plugin_startup.data["oaid"] == ""

    speed = client.rest_request(
        "test_speed_dynamic_url",
        data={"url": "https://example.test/speed", "op": "probe"},
    )
    assert speed.url == "https://example.test/speed"
    assert isinstance(speed.data, dict)
    assert speed.data["op"] == "probe"
    assert "url" not in speed.data


def test_readonly_video_endpoints_are_query_signed():
    client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=FakeSession())

    feed = client.rest_request("video_feed_hot", data={"count": "1"})
    assert feed.method == "POST"
    assert feed.url.endswith("/rest/n/feed/hot")
    assert isinstance(feed.data, dict)
    assert feed.data["count"] == "1"
    assert feed.data["sig"]
    assert feed.data["__NS_sig3"]
    assert feed.params["kpn"] == "KUAISHOU"
    assert feed.params["appver"] == "6.5.5.9591"
    assert feed.params["did"].startswith("ANDROID_")

    comments = client.rest_request("video_photo_comments", data={"photoId": "photo-demo", "count": "1"})
    assert comments.method == "POST"
    assert comments.url.endswith("/rest/n/comment/list/v2")
    assert isinstance(comments.data, dict)
    assert comments.data["photoId"] == "photo-demo"
    assert comments.data["count"] == "1"

    info = client.rest_request("video_photo_info", data={"photoId": "photo-demo"})
    assert info.method == "POST"
    assert info.url.endswith("/rest/n/photo/info")
    assert isinstance(info.data, dict)
    assert info.data["photoIds"] == "photo-demo"
    assert "photoId" not in info.data

    enriched = client.rest_request(
        "video_photo_info",
        data={
            "photoIds": "photo-demo",
            "preUserId": "user-demo",
            "preExpTag": "exp-demo",
            "preLLSId": "llsid-demo",
            "prePhotoIndex": "0",
        },
    )
    assert isinstance(enriched.data, dict)
    assert enriched.data["preUserId"] == "user-demo"
    assert enriched.data["preExpTag"] == "exp-demo"
    assert enriched.data["preLLSId"] == "llsid-demo"


def test_readonly_api_wrapper_shapes_match_retrofit_annotations():
    session = FakeSession()
    client = KwaiClient(
        KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
        assets_root=str(ASSETS),
        session=session,
    )

    client.api_get_photo_infos(["photo-a", "photo-b"])
    method, url, kwargs = session.calls[-1]
    assert method == "POST"
    assert url.endswith("/rest/antman/photo/info")
    assert kwargs["params"]["kpn"] == "KUAISHOU"
    assert kwargs["data"]["photoIds"] == "photo-a,photo-b"
    assert kwargs["data"]["__NStokensig"]

    client.api_comment_list_v2("photo-a", user_id="user-a", count=2, photo_page_type=1)
    _, url, kwargs = session.calls[-1]
    assert url.endswith("/rest/antman/comment/list/v2")
    assert kwargs["data"]["token"] == "token-demo"
    assert kwargs["data"]["photoId"] == "photo-a"
    assert kwargs["data"]["user_id"] == "user-a"
    assert kwargs["data"]["count"] == "2"
    assert kwargs["data"]["photoPageType"] == "1"

    client.api_search("猫", pcursor="cursor-a", ussid="ussid-a")
    _, url, kwargs = session.calls[-1]
    assert url.endswith("/rest/antman/search")
    assert kwargs["data"]["keyword"] == "猫"
    assert kwargs["data"]["pcursor"] == "cursor-a"
    assert kwargs["data"]["ussid"] == "ussid-a"

    client.api_profile_feed("user-a", count=4, privacy="public", referer="feed")
    _, url, kwargs = session.calls[-1]
    assert url.endswith("/rest/antman/feed/profile2")
    assert kwargs["data"]["token"] == "token-demo"
    assert kwargs["data"]["user_id"] == "user-a"
    assert kwargs["data"]["count"] == "4"
    assert kwargs["data"]["referer"] == "feed"

    client.api_get_feed_selection(cold=True, count=5, page=2, cold_start=True)
    _, url, kwargs = session.calls[-1]
    assert url.endswith("/rest/antman/feed/selection")
    assert kwargs["params"]["cold"] == "true"
    assert kwargs["data"]["page"] == "2"
    assert kwargs["data"]["coldStart"] == "true"
    assert kwargs["data"]["count"] == "5"


def test_rest_smoke_only_calls_safe_endpoints():
    session = RoutedFakeSession()
    client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)
    results = client.smoke_test_rest(["version_check", "request_mobile_code"])

    assert results[0]["name"] == "version_check"
    assert results[0]["signature_ok"] is True
    assert results[1]["skipped"] is True
    assert len(session.calls) == 1


def test_rest_info_endpoint_group_is_safe_but_not_default_smoke():
    session = RoutedFakeSession()
    client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)

    assert "video_feed_hot" in INFO_REST_ENDPOINT_NAMES
    assert "api_user_info" in INFO_REST_ENDPOINT_NAMES
    assert "api_search" in REST_INFO_CATEGORIES["search"]
    assert "video_feed_hot" in REST_INFO_CATEGORIES["video"]
    assert "api_user_info" in REST_INFO_CATEGORIES["user"]
    assert "api_message_dialog" in REST_INFO_CATEGORIES["message"]
    assert REST_ENDPOINTS["video_feed_hot"].safe is True
    assert REST_ENDPOINTS["api_user_info"].safe is True
    assert REST_ENDPOINTS["api_message_dialog"].safe is True
    assert REST_ENDPOINTS["api_search_tag_recommend"].rewrite_path is False

    results = client.smoke_test_rest()
    assert {item["name"] for item in results} == {
        "api_experiment",
        "api_startup",
        "plugin_startup",
        "startup",
        "system_speed",
        "version_check",
    }
    assert all("video" not in call[1] for call in session.calls)


def test_info_smoke_category_builds_message_requests_only():
    session = RoutedFakeSession()
    client = KwaiClient(
        KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
        assets_root=str(ASSETS),
        session=session,
    )

    results = client.smoke_test_info_rest(category="message", count=1)

    assert {item["name"] for item in results} == {
        "api_get_friend_users",
        "api_get_share_user_list",
        "api_message_dialog",
        "api_message_load",
        "api_nebula_notify_load",
        "api_news_load",
        "api_notify_load",
    }
    assert all("/rest/antman/" in call[1] for call in session.calls)
    assert any(call[1].endswith("/rest/antman/message/dialog") for call in session.calls)
    assert any(call[1].endswith("/rest/antman/notify/load/v2") for call in session.calls)


def test_private_chat_python_interfaces_shape():
    session = FakeSession()
    device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), session=session)

    client.private_chat_list(count=3, page=1, pcursor="cursor-demo")
    method, url, kwargs = session.calls[-1]
    assert method == "POST"
    assert url.endswith("/rest/antman/message/dialog")
    assert kwargs["data"]["token"] == "token-demo"
    assert kwargs["data"]["count"] == "3"
    assert kwargs["data"]["page"] == "1"
    assert kwargs["data"]["pcursor"] == "cursor-demo"

    client.private_chat_messages("user-demo", page=2, order="asc", pcursor="msg-cursor")
    _, url, kwargs = session.calls[-1]
    assert url.endswith("/rest/antman/message/load")
    assert kwargs["data"]["user_id"] == "user-demo"
    assert kwargs["data"]["page"] == "2"
    assert kwargs["data"]["order"] == "asc"
    assert kwargs["data"]["pcursor"] == "msg-cursor"

    signed = client.private_chat_send("user-demo", "hello", copy=True, send=False)
    assert len(session.calls) == 2
    assert signed.method == "POST"
    assert signed.url.endswith("/rest/antman/message/send")
    assert isinstance(signed.data, dict)
    assert signed.data["user_id"] == "user-demo"
    assert signed.data["content"] == "hello"
    assert signed.data["copy"] == "true"
    assert signed.data["sig"]
    assert signed.data["__NStokensig"] == kwai_token_sig(signed.data["sig"], "salt-demo")


def test_extract_video_ids_includes_detail_context():
    items = KwaiClient.extract_video_ids(
        {
            "llsid": "llsid-demo",
            "feeds": [
                {
                    "photo_id": "photo-demo",
                    "user_id": "user-demo",
                    "exp_tag": "exp-demo",
                    "serverExpTag": "server-exp-demo",
                    "feedLogCtx": {"stidContainer": "stid-demo"},
                }
            ],
        }
    )

    assert items == [
        {
            "photo_id": "photo-demo",
            "user_id": "user-demo",
            "exp_tag": "exp-demo",
            "server_exp_tag": "server-exp-demo",
            "llsid": "llsid-demo",
            "stid_container": "stid-demo",
        }
    ]


def test_account_security_fields_are_real_rsa_signatures():
    device = KwaiDeviceContext(did="did-demo")
    client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
    form = {"mobile": "13800000000"}
    client.add_account_security_fields(form, now_ms=1700000000000)
    first_private_key = device.account_private_key_pem
    second_public_key = client.account_public_key()

    assert form["raw"] == "1700000000000"
    assert form["secret"]
    assert second_public_key
    assert device.account_private_key_pem == first_private_key


def test_phone_login_uses_java_fieldmap_and_original_path():
    device = KwaiDeviceContext(did="did-demo")
    session = FakeSession()
    client = KwaiClient(device, assets_root=str(ASSETS), session=session)

    client.phone_code_login("13800000000", "1234", country_code="86", include_public_key=True)

    method, url, kwargs = session.calls[-1]
    assert method == "POST"
    assert url.endswith("/rest/n/user/login/mobileVerifyCode")
    assert kwargs["data"]["code"] == "1234"
    assert kwargs["data"]["mobileCountryCode"] == "86"
    assert kwargs["data"]["mobile"] == "13800000000"
    assert kwargs["data"]["type"] == "27"
    assert kwargs["data"]["raw"]
    assert kwargs["data"]["secret"]
    assert kwargs["data"]["publicKey"]
    assert kwargs["data"]["deviceName"] == device.mod


def test_register_phone_code_login_uses_plugin_fieldmap_and_original_path():
    device = KwaiDeviceContext(did="did-demo")
    session = FakeSession()
    client = KwaiClient(device, assets_root=str(ASSETS), session=session)

    client.register_by_phone_code_login("13800000000", "1234", country_code="86")

    method, url, kwargs = session.calls[-1]
    assert method == "POST"
    assert url.endswith("/rest/n/user/register/mobileV2")
    assert kwargs["data"]["mobileCode"] == "1234"
    assert kwargs["data"]["mobileCountryCode"] == "86"
    assert kwargs["data"]["mobile"] == "13800000000"
    assert kwargs["data"]["type"] == "302"
    assert kwargs["data"]["publicKey"]
    assert kwargs["data"]["deviceName"] == device.mod
    assert kwargs["data"]["raw"]
    assert kwargs["data"]["secret"]


def test_captures_new_login_client_salt_alias():
    device = KwaiDeviceContext(did="did-demo")
    changed = device.update_from_mapping({"kuaishou.api_client_salt": "salt-demo"}, overwrite=True)

    assert changed["client_salt"] == "salt-demo"
    assert device.client_salt == "salt-demo"


def test_dynamic_field_capture_from_mapping():
    device = KwaiDeviceContext(did="did-demo")
    changed = device.update_from_mapping(
        {
            "data": {
                "cloud_did": "cloud-did-demo",
                "did_tag": "8",
                "egid": "DFP" + "a" * 61,
                "kuaishou.api_st": "api-st-demo",
                "clientSalt": "client-salt-demo",
            }
        }
    )
    assert changed["did"] == "cloud-did-demo"
    assert device.cdid_tag == "8"
    assert device.egid == "DFP" + "a" * 61
    assert device.api_st == "api-st-demo"
    assert device.client_salt == "client-salt-demo"


def test_dynamic_capture_ignores_business_user_ids():
    class FeedResponse(FakeResponse):
        text = '{"result":1}'

        def json(self):
            return {
                "result": 1,
                "feeds": [
                    {"photo_id": "photo-demo", "user_id": "business-user-demo"},
                    {"photoId": "photo-demo-2", "userId": "business-user-demo-2"},
                ],
                "data": {"clientSalt": "client-salt-demo"},
            }

    device = KwaiDeviceContext(did="did-demo", ud="0")
    client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
    changed = client.capture_dynamic_fields(FeedResponse(), request_path="/rest/n/feed/hot")

    assert "ud" not in changed
    assert device.ud == "0"
    assert device.client_salt == "client-salt-demo"


def test_auth_response_can_capture_login_user_id():
    class LoginResponse(FakeResponse):
        text = '{"result":1}'

        def json(self):
            return {"result": 1, "user": {"user_id": "login-user-demo"}}

    device = KwaiDeviceContext(did="did-demo", ud="0")
    client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
    changed = client.capture_dynamic_fields(LoginResponse(), request_path="/rest/n/user/login/mobileVerifyCode")

    assert changed["ud"] == "login-user-demo"
    assert device.ud == "login-user-demo"


def test_import_login_capture_from_har_request_and_response():
    device = KwaiDeviceContext(did="did-demo", ud="0")
    capture = {
        "log": {
            "entries": [
                {
                    "request": {
                        "headers": [
                            {
                                "name": "Cookie",
                                "value": "kuaishou.api_st=api-st-cookie; client_salt=salt-cookie; extra_cookie=extra-demo",
                            }
                        ],
                        "postData": {
                            "text": "token=token-form&client_salt=salt-form&ud=login-user-form",
                            "params": [{"name": "did", "value": "did-from-capture"}],
                        },
                    },
                    "response": {
                        "content": {
                            "text": '{"result":1,"tokenInfo":{"token":"token-json","clientSalt":"salt-json"},"user":{"user_id":"login-user-json"}}'
                        }
                    },
                }
            ]
        }
    }

    changed = device.import_login_capture(capture)

    assert changed["did"] == "did-from-capture"
    assert changed["token"] == "token-json"
    assert changed["api_st"] == "api-st-cookie"
    assert changed["client_salt"] == "salt-json"
    assert changed["ud"] == "login-user-json"
    assert device.cookie_extras["extra_cookie"] == "extra-demo"


def test_import_login_capture_from_raw_headers_and_json_text():
    device = KwaiDeviceContext(did="did-demo")
    raw_capture = """
POST /rest/n/user/login/mobileVerifyCode HTTP/2
Cookie: kuaishou.api_st=api-st-demo; client_salt=salt-cookie

{"result":1,"token":"token-demo","clientSalt":"salt-demo","user":{"user_id":"user-demo"}}
"""

    changed = device.import_login_capture(raw_capture)

    assert changed["token"] == "token-demo"
    assert changed["api_st"] == "api-st-demo"
    assert changed["client_salt"] == "salt-demo"
    assert changed["ud"] == "user-demo"


def test_dfp_signed_form_shape():
    device = KwaiDeviceContext(did="did-demo", rdid="rdid-demo", cdid_tag="8")
    client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
    form = client.dfp_signed_form("deviceInfo", b"abc", now_ms=1700000000000)
    assert form["productName"] == "KUAISHOU"
    assert form["ts"] == "1700000000000"
    assert form["sv"] == "2"
    assert form["rdid"] == "rdid-demo"
    assert form["didtag"] == "8"
    assert form["deviceInfo"]
    assert len(form["sign"]) == 96



def test_device_context_preserves_direct_state_fields():
    device = KwaiDeviceContext.from_mapping(
        {
            "did": "did-demo",
            "token": "token-demo",
            "api_st": "api-st-demo",
            "client_salt": "salt-demo",
            "account_private_key_pem": "pem-demo",
            "cookie_extras": {"token": "token-demo", "kuaishou.api_st": "api-st-demo"},
        }
    )

    assert device.did == "did-demo"
    assert device.token == "token-demo"
    assert device.api_st == "api-st-demo"
    assert device.client_salt == "salt-demo"
    assert device.account_private_key_pem == "pem-demo"
    assert device.cookie_extras == {"token": "token-demo", "kuaishou.api_st": "api-st-demo"}

0x0C 复现命令

从零复核建议按下面顺序:

# 1. 导出证据 bundle 和 manifest
powershell -ExecutionPolicy Bypass -File .\tools\export_kwai_evidence.ps1
Get-Content .\output\kwai_repro_evidence\MANIFEST.tsv -Encoding UTF8

# 2. 校验 Python 语法
python -m py_compile .\kws_recovered\algorithms.py .\kws_recovered\client.py .\kws_client.py

# 3. 跑测试
$env:PYTHONPATH=(Get-Location).Path
pytest -q

# 4. 打印签名请求,不发送
python kws_client.py rest-call video_photo_info --data photoId=123 --print-only

# 5. 查看接口目录
python kws_client.py rest-list --info-only --category search
python kws_client.py rest-list --info-only --category message
python kws_client.py rest-list --info-only --category observed

如果要从 IDA 重新导出 native 伪代码:

IDA Pro -> 打开目标 native IDB/libkwsgmain.so -> File -> Script file...
选择 tools/ida_export_kwai_functions.py
输出 output/ida_reexported/

0x0D 总结:这条链路为什么可信

本文不是把 HAR 字段抄成 Python,也不是把 IDA 伪代码机械翻译成脚本,而是建立了一条互相校验的链:

层级 证据 结论 实现
Retrofit 参数层 ParamsUtils.java sig / __NStokensig 注入位置 kwai_sig() / kwai_token_sig()
主 App profile KwaiParams.java __NS_sig3 输入是 path + sig sign_query() / sign_form_parts()
Java 安全 SDK WebUtils.java / KSecurity.java Java 只包装,算法在 native make_sig3() 分发
Native command IDA ida_sg_command_dispatch_body.c 10405/10417/10418 三代模式 sig3_10405/10417/10418
DFP DFPInitUtils.java KSecurity context 派生设备表单 fetch_egid() / fetch_did()
Azeroth AzerothParamProcessor.java method + path + params canonical azeroth_hmac_client_sign()
REST 工程化 HAR / annotation JSON / client.py endpoint、profile、rewrite、auth、HAR 导入 KwaiClient / CLI
防回归 tests/test_recovered_algorithms.py 关键 shape 可重复验证 pytest -q

这就是为什么最终实现不是一堆魔法常量,而是一套能继续扩展的逆向工程资产。后续如果 App 版本、签名模式、HAR 字段发生变化,继续沿着这条链路查:先看 Java 参数入口,再看 native command,再改 Python 算法,最后补测试。

Comment