# -*- coding: utf-8 -*- """ tdc_profile.py —— 设备画像(fingerprint profile)的**提取**与**定义**。 背景(账本 §五 第 0 项,`har/1ms_dianxuan/PROGRESS.md`): 「纯离线生成」的唯一阻塞点,是需要为每个**语义名**提供取值。 多数语义名是静态环境量(UA / 屏幕 / 时区 / 语言 / 核数 / charset / WebGL / Canvas),可以从一份真机明文里**按值的内容反查**出来。 为什么不靠"明文属于哪份构建": 真机明文的槽顺序 = **它自己那次会话构建**的 require 顺序,而那份 tdc.js 通常没保存(本项目实测:自洽性打分全为负,即明文不属于手上任何构建)。 但**值本身携带强语义**:`"MacIntel"` 只可能是 platform,`"UTF-8"` 只可能是 charset,`"ANGLE (Apple, …)"` 只可能是 webgl_renderer。所以按内容反查 比按位置配对更稳,也不需要那份丢失的构建。 镜像自洽(案例 §6.3): 一套画像内的值必须互相一致 —— screen_fingerprint `"1440-2560-2530-24-…"` 里的 1440/2560/24 应当分别等于 screen_width / screen_height / screen_color_depth(字段序 = 宽-高-可用高-色深)。 本模块会做这个交叉校验。 标准画像请以 `tcaptcha/tdc_values.py::DEVICE_PROFILE` 为准(那里是 37 个语义名 全部对齐后的产物);本文件只是**从明文按值反查**的开发期工具。 用法: # 从一份已解密的真机明文提取画像基线 .venv/bin/python tools/tdc_profile.py from-sample <明文文件> [--json 输出] # 打印内置画像 .venv/bin/python tools/tdc_profile.py show """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any, Dict, List, Optional, Tuple ROOT = Path(__file__).resolve().parents[1] # --------------------------------------------------------------------------- # # 内容反查:值 -> 语义名(可能多个候选,按置信度排序) # # --------------------------------------------------------------------------- # def guess_names(v: Any) -> List[str]: """按**值的内容**反推它可能出自哪个语义名(不依赖槽位置)。""" out: List[str] = [] if isinstance(v, str): if "Mozilla" in v and "AppleWebKit" in v: out.append("user_agent") if v == "MacIntel": out.append("platform") if re.fullmatch(r"[+-]\d{2}(:\d{2})?", v): out.append("timezone_offset") if v.upper() in ("UTF-8", "UTF8"): out.append("charset") if "ANGLE" in v or "Metal Renderer" in v or "OpenGL" in v: out.append("webgl_renderer") if "Inc." in v or ("Google" in v and "Apple" in v): out.append("webgl_vendor") if v.startswith("GgoA") and len(v) > 40: out.append("canvas_fingerprint") if "captcha" in v and "rand=" in v: out.append("template_url") if v == "98k": out.append("constant_98k") if v == "iframe": out.append("iframe_detect") if "-" in v and "*" in v and re.fullmatch(r"[\d\-*|]+", v): out.append("screen_fingerprint") if v.startswith("http") and "1ms.run" in v: out.append("entry_url") elif v.startswith("http"): out.append("location_href") elif isinstance(v, list): if v and all(isinstance(x, list) and len(x) == 3 for x in v): out.append("trajectory") if v and all(isinstance(x, str) for x in v) and 1 <= len(v) <= 4: out.append("languages") elif isinstance(v, int) and not isinstance(v, bool): if 1_600_000_000 <= v <= 2_100_000_000: out.append("timestamp") return out # --------------------------------------------------------------------------- # # 内置画像(MacBook 画像基线;取自真机样本,可按需替换/随机化) # # --------------------------------------------------------------------------- # DEFAULT_PROFILE: Dict[str, Any] = { "name": "Mac / Chrome", # --- 明文里能直接看到的静态量 --- "user_agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"), "platform": "MacIntel", "languages": ["zh-CN", "zh"], "timezone_offset": "+08", "charset": "UTF-8", "hardware_concurrency": 10, # ⚠️ 方向别弄反:真机 screen.width == 1440、screen.height == 2560 # (2026-10-12 浏览器实测;1440x2560 竖屏,availHeight 2530) "screen_width": 1440, "screen_height": 2560, "screen_color_depth": 24, "screen_fingerprint": "1440-2560-2530-24-*-*-|-*", "webgl_renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", "webgl_vendor": "Google Inc. (Apple)", "canvas_fingerprint": ("GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1" "PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg=="), "constant_98k": "98k", "cookie_enabled": 1, "touch_detect": 2, # 真机实测(该机 maxTouchPoints 为 0,值仍是 2) "viewport_size": [360, 360], } def cross_check(profile: Dict[str, Any]) -> List[str]: """做镜像自洽性交叉校验(案例 §6.3)。返回不一致项列表。""" problems: List[str] = [] fp = profile.get("screen_fingerprint") or "" parts = fp.split("-") if len(parts) >= 4: try: # 字段序 = 宽-高-可用高-色深(2026-10-12 与浏览器真值 1440/2560/2530/24 对齐; # 旧代码把它当成 高-宽-?-色深,方向是反的) w, h, _avail, depth = int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]) if h != profile.get("screen_height"): problems.append("screen_fingerprint 的高度 %d != screen_height %s" % (h, profile.get("screen_height"))) if w != profile.get("screen_width"): problems.append("screen_fingerprint 的宽度 %d != screen_width %s" % (w, profile.get("screen_width"))) if depth != profile.get("screen_color_depth"): problems.append("screen_fingerprint 的色深 %d != screen_color_depth %s" % (depth, profile.get("screen_color_depth"))) except ValueError: problems.append("screen_fingerprint 格式无法解析: %r" % fp) ua = profile.get("user_agent") or "" if "Chrome/" in ua and "Macintosh" in ua and profile.get("platform") != "MacIntel": problems.append("UA 说是 Mac,但 platform=%r" % profile.get("platform")) return problems def from_sample(path: str) -> Tuple[Dict[str, Any], List[Dict]]: """从一份真机明文,按内容反查提取画像基线。 返回 (识别到的状态量, 未识别的槽列表)。 """ raw = open(path, encoding="latin1").read().rstrip("\x00") logical = re.sub(r" {2,}", "", raw) cd = json.loads(logical)["cd"] found: Dict[str, Any] = {} unidentified: List[Dict] = [] for i, v in enumerate(cd): cands = guess_names(v) if len(cands) == 1: found[cands[0]] = v elif cands: found.setdefault(cands[0], v) else: unidentified.append({"slot": i, "value": v}) return found, unidentified def main() -> int: ap = argparse.ArgumentParser(description="TDC 设备画像提取/查看") sub = ap.add_subparsers(dest="cmd", required=True) p1 = sub.add_parser("from-sample", help="从真机明文提取画像基线") p1.add_argument("plain") p1.add_argument("--json", default=None) sub.add_parser("show", help="打印内置画像") args = ap.parse_args() if args.cmd == "show": print(json.dumps(DEFAULT_PROFILE, ensure_ascii=False, indent=1)) probs = cross_check(DEFAULT_PROFILE) print() print("交叉校验:", "全部自洽" if not probs else probs) return 0 found, unk = from_sample(args.plain) print("从 %s 反查到的状态量:" % Path(args.plain).name) for k, v in found.items(): print(" %-22s %s" % (k, json.dumps(v, ensure_ascii=False)[:70])) print() print("未识别的槽: %d 个" % len(unk)) for u in unk: print(" 槽%-3d %s" % (u["slot"], json.dumps(u["value"], ensure_ascii=False)[:60])) print() probs = cross_check(found) print("交叉校验:", "全部自洽 ✓" if not probs else probs) if args.json: Path(args.json).write_text(json.dumps(found, ensure_ascii=False, indent=1), encoding="utf-8") print("画像 -> %s" % args.json) return 0 if __name__ == "__main__": raise SystemExit(main())