feat(collect): 按 docs/tdc-案例.md 重做主链路 —— 四段加密 + 字符串指纹识别 37/37

做的什么:
  1. 判决并修正 collect 结构: 不是「整体一次加密」, 而是
     chunk1 / trajectory / chunk2 / sd **四段独立加密**, 前 3 段
     空格 pad 到 24 字节对齐、sd 不 pad, 4 段 base64 直接拼接。
  2. 按案例 §5.3 实现字符串+结构指纹的模块识别器, 把 37 个采集器
     全部识别且跨构建唯一。
  3. 修 reorder 的恒等映射 bug, 并补强断言。
  4. 在线抓取全新样本作为唯一数据源, 清理探索性脚本。

为什么:
  * 旧「整体加密 + \x00 补齐」与真机数据矛盾 —— 导致多轮实验
    建立在结构错误的 collect 上(这正是此前反复"乱碰"的根因)。
  * 模块 id 每份构建重排, 不能当跨构建锚点; 但模块的**代码**
    (字符串集合、结构)不变 —— 这是案例指明的不变量。
  * reorder 旧实现用「目标槽下标」切「源数组」, 顺序不同即全盘错位;
    原测试只断言"是置换", 恒等函数也满足, 所以 bug 被放过了。

怎么验证:
  * 四段结构(3 条独立证据, 其中一条不需密钥):
      - 真机明文 3 处连续空格串 [+5,+10,+24], 且 pad == 24-len%24 全中
      - 密文长度算术 480+96+416+56 = 1048 == 真实 1048 字符
      - '=' 只出现在 1046/1048, 对应 sd 段 33B→补 7→40B
  * 指纹: 4 份全新在线构建 37/37 识别、0 unknown、跨构建语义名集合
    完全一致、单构建内 0 重名
  * 配对正确性: 独立证据 37/37 一致; 负对照(清单下标 / 模块 id /
    随机配对)均只有 2-3/37 —— 证明归一化没有过拟合
  * 端到端重排 3/3 正确且非恒等
  * selftest 全绿; 强断言自查: 旧实现过不了新断言

新增:
  tools/tdc_modules.py            字符串+结构指纹识别(案例 §5.3)
  scripts/fetch_online_samples.py 在线抓取全新 tdc.js
  scripts/verify/*.py             7 个可复现的验证脚本
  docs/tdc-案例.md                案例原文(看雪, 作者执着的猫)

修改:
  tcaptcha/crypto.py          +build_collect / split_collect_segments(四段)
  tools/tdc_collectgen.py     reorder 改为跨构建锚点对齐 + spec_from_seq
  tools/tdc_layout.py         +static_fingerprint / opcode_histogram
  tools/tdc_program.py        修符号求值插值类型崩溃
  tcaptcha/tests/test_collectgen.py  补「非恒等」强断言 + 在线新样本端到端
  tcaptcha/client.py          debug 目录改到系统临时目录(不再写仓库内 scratch/)
  har/1ms_dianxuan/PROGRESS.md      新增已知 #41~#49, 显式修正被推翻的旧结论
  .gitignore                  +scratch/  +har/online/

删除:
  scratch/(含 10-11 遗留的一次性脚本, 已确认不可恢复后清理)

注意:
  * 账本记录了一次自我纠错: 曾据案例 §5.5 误判 kind=2「不进 cd」,
    实测(cd 38 槽、轨迹在第 10 槽)否定后已撤回。
  * 仍未打通: 纯离线生成需要每个语义名的取值来源(已知 #38)。
This commit is contained in:
yml2213
2026-10-12 02:28:08 +08:00
parent 50734a854b
commit 80a122245a
18 changed files with 2512 additions and 40 deletions
+78 -3
View File
@@ -10,8 +10,9 @@
4. 加密/解密**回环一致**(明文逐字节还原);
5. 槽数不匹配的样本**必须报错**,不能静默错位。
样本: tcaptcha/tests/fixtures/tdc_slotmap_case.json
= {seq, kinds, v0s, sample_cd},取自 2026-10-11 真机会话(37 项 -> 38 槽)。
样本:
* tcaptcha/tests/fixtures/tdc_slotmap_case.json —— 合成用例(已知正确答案)
* har/online/<时间戳>/tdc_*.js —— **在线抓取的全新构建**(真机数据)
运行:
.venv/bin/python -m tcaptcha.tests.test_collectgen
@@ -88,11 +89,85 @@ def main() -> int:
print("\n[5] 槽数不匹配必须报错")
raised = False
try:
cg.reorder(sample_cd[:-1], spec)
cg.reorder(sample_cd[:-1], spec) # src=None: 契约是"同序"
except ValueError:
raised = True
check("少一槽 -> ValueError", raised)
# ---------------------------------------------------------------- #
# [6] 强断言:重排**必须真的重排**(旧实现的 bug 就是被"是置换"放过的)
# ---------------------------------------------------------------- #
print("\n[6] reorder 不是恒等映射(抓旧 bug)")
print(" 旧实现的循环是 sample[slot:slot+n],其中 slot 是**目标**槽号,")
print(" 两份构建顺序不同时全盘错位;而'值集合不变'这个弱断言抓不住它。")
# 把清单顺序打乱,构造"另一份构建"
import random
idx = list(range(len(seq)))
random.Random(11).shuffle(idx)
seq2 = [seq[i] for i in idx]
kinds2 = [kinds[i] for i in idx]
v0s2 = [v0s[i] for i in idx]
# 给每项一个唯一语义名(真实场景由 tdc_modules 的指纹提供)
names1 = ["n%d" % i for i in range(len(seq))]
names2 = ["n%d" % i for i in idx]
src = cg.spec_from_seq(seq, kinds, names1, sample_cd=sample_cd, v0s=v0s)
dst = cg.spec_from_seq(seq2, kinds2, names2, v0s=v0s2)
# 正确答案:按 src 的 assign 把每项的值搬到目标顺序
truth: list = []
for i in idx:
_i, _m, sl, nn = src.assign[i]
truth.extend(sample_cd[sl:sl + nn])
try:
out2 = cg.reorder(sample_cd, dst, src)
check("重排结果 == 已知正确答案", out2 == truth)
check("重排结果 != 原样(证明真的重排了)", out2 != list(sample_cd))
except cg.UnmappedSlotsError as exc:
check("重排结果 == 已知正确答案", False, f"抛错: {str(exc)[:80]}")
# ---------------------------------------------------------------- #
# [7] 用在线抓取的**全新构建**做端到端重排(真机数据,非合成)
# ---------------------------------------------------------------- #
online = sorted((ROOT / "har" / "online").glob("*/tdc_*.js")) if (
ROOT / "har" / "online").exists() else []
print("\n[7] 在线新构建 + 语义名锚点的端到端重排")
if len(online) < 2:
print(" (跳过: 需要 ≥2 份 har/online/*/tdc_*.js;"
"用 scripts/fetch_online_samples.py --n 4 抓取)")
else:
try:
import tdc_modules
except Exception as exc:
check("导入 tdc_modules", False, str(exc)[:80])
online = []
if len(online) >= 2:
def names_of(f):
return [r["name"] for r in tdc_modules.analyze(f)]
na = names_of(online[0])
n = len(na)
check("新构建清单 37 项", n == 37, "实际 %d" % n)
check("语义名全部唯一(37/37)", len(set(na)) == n,
"唯一 %d/%d" % (len(set(na)), n))
check("无 unknown", not any(x == "unknown" for x in na))
# 给每个语义名一个假值,按 A 顺序排成 cd_A,重排到 B 顺序
fake = {nm: "V<%s>" % nm for nm in na}
cd_a = [fake[nm] for nm in na]
seqa = [r["module"] for r in tdc_modules.analyze(online[0])]
srca = cg.spec_from_seq(seqa, [None] * n, na, sample_cd=cd_a)
all_ok = True
for f in online[1:]:
nb = names_of(f)
seqb = [r["module"] for r in tdc_modules.analyze(f)]
dstb = cg.spec_from_seq(seqb, [None] * len(nb), nb)
truthb = [fake[nm] for nm in nb]
try:
got = cg.reorder(cd_a, dstb, srca)
good = got == truthb and got != cd_a
except Exception:
good = False
all_ok &= good
print(" %-28s 重排正确=%s" % (f.parent.name + "/" + f.name[:12], good))
check("全部新构建重排正确且非恒等", all_ok)
print(f"\n{'=' * 62}")
if _fails:
print(f"❌ collectgen 测试失败: {_fails}")