# -*- coding: utf-8 -*- """verify_offline.py —— 纯离线生成一颗 collect,再自己解回来对拍(闭环验证)。 链路(全程零浏览器、零 Node 运行时;密钥来自纯 Python 静态提取): tdc.js -> tdc_modules.analyze() 37 个语义名(清单序 = 槽位序) -> tdc_values.generate_cd() 38 个槽的取值 -> tdc_values.build_plain() 4 段明文 -> crypto.build_collect() 4 段各自 XTEA + base64 拼接 -> 解密 / 切段 / 解析 应当逐槽回到原值 同时校验**长度算术**:前三段 pad 到 24 的倍数 -> base64 无 '=' 填充; sd 段不 pad -> 允许 '=' 且只出现在末尾。 用法: .venv/bin/python scripts/verify/verify_offline.py /tmp/1ms_bridge/session_build.js """ import base64 import json import sys from pathlib import Path from urllib.parse import unquote ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) import tdc_modules as tm # noqa: E402 from tcaptcha.crypto import (build_collect, split_collect_segments, # noqa: E402 xtea_decrypt_bytes) from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 from tcaptcha.tdc_values import Session, build_plain, generate_cd, trajectory_index # noqa: E402 def main() -> int: build = sys.argv[1] if len(sys.argv) > 1 else "/tmp/1ms_bridge/session_build.js" js = open(build, encoding="utf-8", errors="replace").read() key = extract_xtea_key_pure(js)["key"] rows = tm.analyze(build) unknown = [r["name"] for r in rows if r["name"] == "unknown"] sess = Session() cd = generate_cd(rows, sess) plain = build_plain(cd, rows) k = trajectory_index(rows) print("构建 : %s (%d 字节)" % (Path(build).name, len(js))) print("清单 : %d 项, 未识别 %d" % (len(rows), len(unknown))) print("槽 : %d 个(轨迹在第 %d 槽)" % (len(cd), k)) print("段明文 : chunk1=%d traj=%d chunk2=%d sd=%d" % (len(plain["chunk1"]), len(plain["trajectory"]), len(plain["chunk2"]), len(plain["sd"]))) for nm in ("chunk1", "trajectory", "chunk2"): n = len(plain[nm]) print(" %-10s pad %2d -> %d (=24*%d)" % (nm, 24 - n % 24, n + 24 - n % 24, (n + 24 - n % 24) // 24)) collect = build_collect(plain["chunk1"], plain["trajectory"], plain["chunk2"], plain["sd"], key) print("collect : %d 字符, '=' 出现在 %s" % (len(collect), [i for i, c in enumerate(collect) if c == "="] or "无")) # ---- 解回来对拍 ---- dec = xtea_decrypt_bytes(base64.b64decode(unquote(collect)), key) \ .decode("latin1").rstrip("\x00") c1, traj, c2, sd = split_collect_segments(dec) got = json.loads(c1 + traj + c2 + sd)["cd"] ok = got == cd print() print("解回槽数 : %d" % len(got)) print("逐槽全等 : %s" % ("是" if ok else "否")) if not ok: for i, (o, g) in enumerate(zip(cd, got)): if o != g: print(" 槽%-3d 原=%s 回=%s" % (i, json.dumps(o, ensure_ascii=False)[:40], json.dumps(g, ensure_ascii=False)[:40])) # 分段几何也要一致 geo = (len(c1), len(traj), len(c2), len(sd)) == \ (len(plain["chunk1"]), len(plain["trajectory"]), len(plain["chunk2"]), len(plain["sd"])) print("分段几何 : %s" % ("一致" if geo else "不一致")) return 0 if (ok and geo) else 1 if __name__ == "__main__": raise SystemExit(main())