feat(collect): 纯离线生成打通 1ms 签到文字点选 —— errorCode=0 收口
做了什么:
- 新增 tcaptcha/tdc_values.py: 37 个语义名的取值生成器(静态画像 / 会话量 /
每次采集量三层)+ build_plain 四段组装 + generate_collect 纯离线出包
- crypto: 新增 generate_telemetry(mode="python_values");轨迹生成统一到
trajectory_points()(首点绝对坐标 + 增量 + 固定收尾 [1,1,12])
- crypto.split_collect_segments 重写: pad 只有 1 个空格时旧 ` {2,}` 判据会漏段,
且 UA 串里的普通空格会假阳性 —— 改为「模 24 算术 + 不落在 JSON 字符串内部 + 结构校验」
- tools/tdc_modules: trajectory 指纹原先指向只做 JSON.stringify 的模块,改为
mouseMoveEvent/touchStartEvent(真轨迹模块),37/37 不变
- client 默认 telemetry_mode 改为 python_values
- 新增 scripts/verify/{align_session,audit_names,verify_values,verify_offline,send_test}.py
与 tools/{cdp_collect,cdp_dump_scripts,tdc_profile}.py
为什么:
- 账本 #40「谁在发 HTTP 是 12 的剩余自变量」被推翻。同 aid/同客户端/同求解器、
答案都对,只换 collect 构造方式: 本次会话构建生成 -> 0 (4/4),
旧模板回填 -> 12 (2/2)。真因是 collect 必须与本次会话构建同源。
怎么验证:
- .venv/bin/python -m tcaptcha.selftest 全绿(含新增四段切分与纯离线闭环回归)
- scripts/verify/verify_values.py 4 份真机明文 152/152 逐槽全等
- scripts/verify/verify_offline.py 逐槽全等 + 分段几何一致
- scripts/verify/send_test.py --mode python_values 1ms 签到页文字点选 errorCode=0 x4
This commit is contained in:
@@ -5,25 +5,141 @@
|
||||
固化:
|
||||
1. 教科书 XTEA 加解密往返 (含 8 字节对齐补齐)
|
||||
2. encrypt_string 与 xtea_encrypt_bytes 结果一致 (防止两份 block 实现再次漂移)
|
||||
3. **四段切分**的实测回归:真机存在「pad 只有 1 个空格」的样本,
|
||||
旧的 ` {2,}` 判据会直接崩(2026-10-12 踩坑,账本 #50/#51)
|
||||
4. **纯离线闭环**:tdc.js -> 语义名 -> 槽取值 -> 4 段明文 -> 加密 -> 解回
|
||||
|
||||
运行:
|
||||
.venv/bin/python -m tcaptcha.tests.test_crypto_core
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if str(ROOT / "tools") not in sys.path:
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
from tcaptcha.crypto import selfcheck # noqa: E402
|
||||
from tcaptcha.crypto import (build_collect, selfcheck, split_collect_segments, # noqa: E402
|
||||
xtea_decrypt_bytes)
|
||||
|
||||
# 真机同会话 collect 的**解密明文**(2026-10-12,1ms aid=195672344 的验证码 iframe)。
|
||||
# 它的价值在于:chunk1 = 287 字节,287 % 24 == 23 -> **pad 只有 1 个空格**,
|
||||
# 而那个空格又恰好满足 `b%24==0 且 (b-a)==24-a%24` —— 两个坑它一次占全。
|
||||
REAL_PLAIN_B64 = (
|
||||
"eyJjZCI6WzI1NjAsMCw2NDE1NDc0ODksMTc5MTczMjAwMSwiTW96aWxsYS81LjAgKE1hY2ludG9z"
|
||||
"aDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlr"
|
||||
"ZSBHZWNrbykgQ2hyb21lLzE1NS4wLjAuMCBTYWZhcmkvNTM3LjM2IiwwLCJVVEYtOCIsWzM2MCwz"
|
||||
"NjBdLDAsIk1hY0ludGVsIixbInpoLUNOIiwiemgiXSwxMCwiMTQ0MC0yNTYwLTI1MzAtMjQtKi0q"
|
||||
"LXwtKiIsIiIsW10sMTc5MTc0NTU0OCwyLCIrMDgiLCJHb29nbGUgSW5jLiAoQXBwbGUpIiwwLDAs"
|
||||
"MCwgW1sxNCwxMSwzMjI5NzFdLFszLDAsN10sWzQsMSw3XSxbNSwtMSw3XSxbNiwwLDddLFsyLDEs"
|
||||
"N10sWzMsLTEsNl0sWzQsMCw3XSxbNSwxLDddLFs2LC0xLDddLFsyLDAsN10sWzMsMSw3XSxbNCwt"
|
||||
"MSw2XSxbNSwwLDddLFs2LDEsN10sWzIsLTEsN10sWzMsMCw3XSxbNCwxLDddLFs1LC0xLDddLFs2"
|
||||
"LDAsN10sWzIsMSw3XSxbMywtMSw3XSxbNCwwLDddLFs1LDEsN10sWzYsLTEsN10sWzIsMCw2XSxb"
|
||||
"MywxLDddLFs0LC0xLDddLFs1LDAsN10sWzYsMSw3XSxbMiwtMSw3XSxbMywwLDddLFs0LDEsNl0s"
|
||||
"WzUsLTEsN10sWzYsMCw3XSxbMiwxLDddLFszLC0xLDddLFs0LDAsN10sWzUsMSw3XSxbNiwtMSw3"
|
||||
"XSxbMiwwLDddLFszLDEsN10sWzQsLTEsNl0sWzUsMCw3XSxbNiwxLDddLFsyLC0xLDddLFszLDAs"
|
||||
"Nl0sWzQsMSw3XSxbNSwtMSw3XSxbNiwwLDddLFsyLDEsN10sWzMsLTEsN10sWzQsMCw3XSxbNSwx"
|
||||
"LDddLFs2LC0xLDddLFsyLDAsN10sWzMsMSw2XSxbNCwtMSw4XSxbNSwwLDddLFs2LDEsN10sWzEs"
|
||||
"MSwxMl1dICAgICAgICAgICAgLCIiLDEwMjMsMTU5MTkxMjQ4MiwyLCJodHRwczovLzFtcy5ydW4v"
|
||||
"Y29uc29sZS9wZXJzb25hbC9iZW5lZml0cy9jaGVjay1pbiIsIkdnb0FBQUFOU1VoRVVnQUFBU3dB"
|
||||
"QUFDV0NBWUFBQVRmM3Y4YmgrcnlXY2ZTQlNMa21OM04xUFFpblBMN0JpM1RHMHFFMkNvZ0FBQUFC"
|
||||
"SlJVNUVya0pnZ2c9PSIsMSwiQU5HTEUgKEFwcGxlLCBBTkdMRSBNZXRhbCBSZW5kZXJlcjogQXBw"
|
||||
"bGUgTTQsIFVuc3BlY2lmaWVkIFZlcnNpb24pIiwxNzkxNzQ1MjI0LCJpZnJhbWUiLDE0NDAsIiIs"
|
||||
"Imh0dHBzOi8vdHVyaW5nLmNhcHRjaGEuZ3RpbWcuY29tLzEvdGVtcGxhdGUvZHJhZ19lbGUuaHRt"
|
||||
"bD9yYW5kPTE1MTk3MTM2MjQzNDciLDI0LCI5OGsiXSwgICAgInNkIjp7Im9kIjoiQyJ9fQ=="
|
||||
)
|
||||
|
||||
|
||||
def test_split_single_space_pad() -> None:
|
||||
"""真机明文:chunk1 的 pad 只有 1 个空格,旧判据会漏。"""
|
||||
plain = base64.b64decode(REAL_PLAIN_B64).decode("latin1")
|
||||
assert len(plain) == 1192
|
||||
|
||||
# 旧判据(` {2,}`)只看得见 2 个 pad -> 切不出 4 段(这就是被修掉的 bug)
|
||||
assert len(re.findall(r" {2,}", plain)) == 2
|
||||
|
||||
c1, traj, c2, sd = split_collect_segments(plain)
|
||||
assert (len(c1), len(traj), len(c2), len(sd)) == (287, 516, 356, 16)
|
||||
assert c1.startswith('{"cd":[') and c1.endswith(",")
|
||||
assert c2.startswith(",") and c2.endswith("],")
|
||||
assert sd == '"sd":{"od":"C"}}'
|
||||
|
||||
obj = json.loads(c1 + traj + c2 + sd)
|
||||
assert len(obj["cd"]) == 38
|
||||
points = json.loads(traj)
|
||||
assert len(points) == 61 and points[0] == [14, 11, 322971]
|
||||
assert points[-1] == [1, 1, 12]
|
||||
|
||||
|
||||
def test_split_ignores_space_inside_string() -> None:
|
||||
"""UA 串里的普通空格在偏移 47 处也满足 pad 模算术,不能被当成 pad。"""
|
||||
plain = base64.b64decode(REAL_PLAIN_B64).decode("latin1")
|
||||
assert plain[47] == " " and 47 % 24 == 23 and 48 % 24 == 0 # 它确实满足判据
|
||||
c1, _traj, _c2, _sd = split_collect_segments(plain)
|
||||
assert '"Mozilla/5.0 (Macintosh' in c1 # 空格仍在字符串里,没被切掉
|
||||
|
||||
|
||||
def test_build_collect_roundtrip_single_space_pad() -> None:
|
||||
"""自己造一份 pad==1 的样本,验证 加密 -> 切分 可逆。"""
|
||||
key = [0x11111111, 0x22222222, 0x33333333, 0x44444444]
|
||||
chunk1 = '{"cd":[' + "1," * 20 # 7 + 40 = 47, 47 % 24 == 23 -> pad 1
|
||||
traj = "[[1,1,12]]"
|
||||
chunk2 = ',"x"],'
|
||||
sd = '"sd":{"od":"C"}}'
|
||||
assert len(chunk1) % 24 == 23
|
||||
|
||||
collect = build_collect(chunk1, traj, chunk2, sd, key)
|
||||
plain = xtea_decrypt_bytes(base64.b64decode(collect), key).decode("latin1").rstrip("\x00")
|
||||
|
||||
assert plain[47:48] == " " # 第一段的 pad 就是一个空格
|
||||
assert split_collect_segments(plain) == (chunk1, traj, chunk2, sd)
|
||||
|
||||
|
||||
def test_offline_generate_and_roundtrip() -> None:
|
||||
"""纯离线:tdc.js -> 37 语义名 -> 38 槽 -> 4 段明文 -> 加密 -> 解回逐槽相等。"""
|
||||
import tdc_modules as tm
|
||||
from tcaptcha.tdc_key import extract_xtea_key_pure
|
||||
from tcaptcha.tdc_values import Session, build_plain, generate_cd, trajectory_index
|
||||
|
||||
build = ROOT / "tcaptcha" / "tdc_sample.js"
|
||||
js = build.read_text(encoding="utf-8", errors="replace")
|
||||
key = extract_xtea_key_pure(js)["key"]
|
||||
|
||||
rows = tm.analyze(str(build))
|
||||
assert len(rows) == 37
|
||||
assert not [r for r in rows if r["name"] == "unknown"]
|
||||
|
||||
cd = generate_cd(rows, Session())
|
||||
assert len(cd) == 38 # itoken 占 2 槽
|
||||
plain = build_plain(cd, rows)
|
||||
k = trajectory_index(rows)
|
||||
assert cd[k] == [[1, 1, 12]] # 轨迹槽
|
||||
|
||||
collect = build_collect(plain["chunk1"], plain["trajectory"],
|
||||
plain["chunk2"], plain["sd"], key)
|
||||
dec = xtea_decrypt_bytes(base64.b64decode(collect), key).decode("latin1").rstrip("\x00")
|
||||
c1, traj, c2, sd = split_collect_segments(dec)
|
||||
assert json.loads(c1 + traj + c2 + sd)["cd"] == cd
|
||||
assert (len(c1), len(traj), len(c2), len(sd)) == \
|
||||
(len(plain["chunk1"]), len(plain["trajectory"]),
|
||||
len(plain["chunk2"]), len(plain["sd"]))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
selfcheck()
|
||||
print("✅ crypto 纯算法自检通过 (XTEA 往返 + encrypt_string 一致性)")
|
||||
test_split_single_space_pad()
|
||||
test_split_ignores_space_inside_string()
|
||||
test_build_collect_roundtrip_single_space_pad()
|
||||
test_offline_generate_and_roundtrip()
|
||||
print("✅ crypto 纯算法自检通过 (XTEA 往返 + encrypt_string 一致性"
|
||||
" + 四段切分回归 + 纯离线闭环)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user