From faccc8431a4f9a7e924103fca47818bbcf7c97d8 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Sat, 25 Apr 2026 14:43:20 +0800 Subject: [PATCH] docs: add secspider design spec --- .../specs/2026-04-25-secspider-design.md | 524 ++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-25-secspider-design.md diff --git a/py/docs/superpowers/specs/2026-04-25-secspider-design.md b/py/docs/superpowers/specs/2026-04-25-secspider-design.md new file mode 100644 index 0000000..8a06461 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-25-secspider-design.md @@ -0,0 +1,524 @@ +# SecSpider 加密插件设计 + +## 目标 + +为当前 Spider 插件体系设计一套可落地的“源码封包”机制,用于隐藏 Spider 明文源码,避免普通用户直接打开插件文件即可看到实现细节。 + +本次设计面向的宿主前提如下: + +- 宿主支持修改加载流程 +- 插件既可能来自本地文件,也可能来自远程下载 +- 宿主本身是开源可见、运行在用户设备上的 Python 客户端 +- 现有 Spider 插件接口保持不变,仍以 `Spider` 类作为宿主入口 + +本次设计的核心目标是: + +- 让发布物不再是直接可读的 Python 明文源码 +- 支持宿主对插件做来源校验和完整性校验 +- 在不改变 Spider 对外接口的前提下完成加载 +- 保持明文插件与加密插件双栈兼容,便于渐进迁移 + +本次设计不追求: + +- 防住能修改宿主、抓内存、下断点的逆向者 +- 做在线授权平台 +- 做设备绑定、过期控制或远程吊销 +- 让用户设备上的插件“不可提取” + +## 背景与边界 + +当前宿主 `SpiderPluginLoader` 使用 `importlib.util.spec_from_file_location(...)` 直接执行本地 Python 文件,并约定模块中导出 `Spider` 类。该机制适合明文源码插件,但不适合密文封包。 + +由于本次目标是隐藏源码而不是彻底防逆向,设计必须先承认以下边界: + +- 只要插件最终要在本地运行,明文源码或等价字节码一定会在某个时刻进入解释器 +- 解密端开源且跑在用户设备上,因此任何宿主内置密钥都可能被逆向提取 +- 这意味着方案的安全收益是“提高获取源码的门槛”,而不是“保证源码永不泄露” + +因此,本次设计定位为: + +- 源码封包机制 +- 完整性与签名校验机制 +- 宿主侧双栈插件加载机制 + +而不是 DRM 或不可导出的执行环境。 + +## 方案选择 + +候选方案: + +1. 纯混淆方案 +2. 离线封包 + 本地验签 + 本地解密方案 +3. 在线授权下发解密材料方案 + +选用方案 2。 + +原因: + +- 用户当前目标只是避免普通用户直接看源码,不需要引入在线依赖 +- 宿主已允许修改,适合增加自定义 loader +- 本地验签可以防止第三方伪造或篡改插件 +- 双栈方案能平滑兼容现有明文插件生态 +- 相比在线授权,工程复杂度和运维成本更可控 + +不选方案 1 的原因: + +- 只能防止双击直读,无法提供可靠的篡改检测 +- 无法建立统一的协议版本、密钥轮换和签名链路 + +不选方案 3 的原因: + +- 超出当前目标 +- 需要额外服务端、授权链路和可用性保障 + +## 总体架构 + +系统分成三个角色: + +- `builder` + - 读取明文 Spider 源码 + - 生成内容密钥 + - 加密源码 + - 生成签名 + - 输出单文件加密包 +- `loader` + - 读取插件文件 + - 识别明文或密文格式 + - 对密文包执行验签、解密、内存加载 +- `runtime spider` + - 解密后在内存中编译执行 + - 对宿主继续暴露 `Spider` 类 + +总体链路如下: + +1. 构建阶段读取明文 Spider 源码 +2. 生成随机内容密钥 `content_key` +3. 用 `content_key` 对源码做对称加密 +4. 使用宿主密钥派生链路包裹 `content_key` +5. 对包头和密文内容做签名 +6. 产出单文件文本封包 +7. 宿主加载时先识别格式 +8. 若为明文插件,则继续按现有 `importlib` 逻辑执行 +9. 若为密文插件,则先验签,再解包内容密钥,再解密源码 +10. 解密后的源码通过 `compile/exec` 在内存中执行 +11. 宿主统一从模块中读取 `Spider` 类并实例化 + +## 封包格式设计 + +建议定义文本协议 `secspider/1`,保持现有注释头风格,便于远程分发和人工排查。 + +推荐格式: + +```text +// ignore +//@name:[直] omofun +//@webSite:http://101.42.227.94:11024 +//@version:1 +//@remark: +//@codeID:z5nxgZSWE8ipZYvGtImsRFu73TOB45fZ +//@env: +//@isAV:0 +//@deprecated:0 +//@order:B +//@format:secspider/1 +//@alg:aes-256-gcm +//@wrap:hkdf-aes-keywrap +//@sign:ed25519 +//@kid:k2026_04 +//@nonce:base64:... +//@ek:base64:... +//@hash:sha256:... +//@sig:base64:... +// ignore +payload.base64:... +``` + +字段分组如下: + +- 业务元数据 + - `name` + - `webSite` + - `version` + - `remark` + - `codeID` + - `env` + - `isAV` + - `deprecated` + - `order` +- 协议元数据 + - `format` + - `alg` + - `wrap` + - `sign` + - `kid` +- 加密与校验字段 + - `nonce` + - `ek` + - `hash` + - `sig` +- 数据字段 + - `payload.base64` + +字段约束如下: + +- `format` 固定为 `secspider/1` +- `alg` 首版固定为 `aes-256-gcm` +- `wrap` 首版固定为 `hkdf-aes-keywrap` +- `sign` 首版固定为 `ed25519` +- `kid` 表示当前包使用的密钥版本 +- `nonce` 为内容加密随机数 +- `ek` 为包裹后的内容密钥,不是主密钥本身 +- `hash` 为解密前明文源码的 `sha256` +- `sig` 覆盖除 `sig` 自身外的所有头字段和 `payload` + +`payload.base64` 存放最终密文载荷,推荐流程为: + +- 明文源码转 `utf-8` 字节 +- 可选压缩 +- 使用内容密钥加密 +- 对结果做 `base64` 编码 + +## 算法选择 + +首版固定采用如下组合: + +- 内容加密:`AES-256-GCM` +- 内容密钥派生:`HKDF-SHA256` +- 发布签名:`Ed25519` + +选择原因如下: + +- `AES-256-GCM` 为成熟 AEAD 算法,适合同时提供保密性和密文完整性 +- `HKDF-SHA256` 足够简单,适合从宿主主密钥按 `kid/codeID/version` 派生包密钥 +- `Ed25519` 适合快速、稳定地完成离线签名和验签 + +本次设计明确不采用: + +- 自定义异或或字符串切片混淆 +- 自研加解密协议 +- 只做加密不做签名 + +## 密钥分层设计 + +宿主开源且运行在用户设备上,因此密钥体系的设计目标不是“绝对保密”,而是避免所有插件共用一把固定明文密钥。 + +推荐分三层密钥: + +### 1. 发布签名密钥对 + +- 构建环境持有 `Ed25519 private key` +- 宿主持有对应 `Ed25519 public key` + +用途: + +- 证明插件确实由发布方签发 +- 拦截被篡改或伪造的插件包 + +约束: + +- 私钥绝不进入客户端 + +### 2. 宿主主密钥 + +- 宿主内置 `master_secret` +- 不直接用于解密 `payload` + +用途: + +- 派生当前包使用的 `wrap_key` + +说明: + +- 这层密钥会被逆向到,因此安全作用有限 +- 但仍优于“所有插件共用固定 AES key”的做法 + +### 3. 内容密钥 + +- 每个插件构建时随机生成一个 32 字节 `content_key` + +用途: + +- 只用于当前插件包的源码加密 + +优势: + +- 每个插件包独立加密 +- 不同包之间不共享实际内容密钥 + +### 密钥派生规则 + +建议: + +```text +wrap_key = HKDF-SHA256( + ikm = master_secret, + salt = kid, + info = "secspider:" + codeID + ":" + version +) +``` + +用途: + +- `wrap_key` 用于包裹或解包 `content_key` + +收益: + +- 同一 `master_secret` 下,不同 `codeID/version` 的包不会直接共钥 +- 支持按 `kid` 做宿主侧密钥轮换 + +## 密钥轮换策略 + +建议宿主维护一个 `keyring`,按 `kid` 管理签名公钥和主密钥材料。 + +设计要求: + +- 插件头部必须携带 `kid` +- loader 先按 `kid` 查找密钥材料 +- 找不到 `kid` 直接拒绝执行 +- 新版插件可切换到新 `kid` +- 旧 `kid` 可在兼容期内继续保留 + +首版只需要支持静态内置 `keyring`,不要求联网拉取。 + +## 宿主加载设计 + +### 现状 + +当前宿主 `SpiderPluginLoader` 的核心流程为: + +1. 安装兼容 `base.spider` +2. 通过本地路径或远程下载得到插件文件 +3. 通过 `importlib.util.spec_from_file_location(...)` 执行源码文件 +4. 从模块中读取 `Spider` 类并实例化 + +这条链路仅适合明文 Python 文件。 + +### 目标形态 + +宿主改造为双栈加载: + +- 明文插件:保持现有 `importlib` 路径 +- 加密插件:走自定义 `secspider` runtime 路径 + +两条路径最终都返回 `types.ModuleType`,上层实例化逻辑保持不变。 + +### 推荐改造点 + +在 `SpiderPluginLoader` 中新增三个私有方法: + +- `_detect_package_format(source_path) -> str` +- `_load_plain_module(module_name, source_path) -> types.ModuleType` +- `_load_secspider_module(module_name, source_path, config) -> types.ModuleType` + +主流程调整为: + +1. `_install_compat_modules()` +2. `_resolve_source_path()` +3. `_detect_package_format(source_path)` +4. 如果不是 `secspider/1`,则走 `_load_plain_module(...)` +5. 如果是 `secspider/1`,则走 `_load_secspider_module(...)` +6. 统一读取 `Spider` 类、实例化并执行 `init(config.config_text)` + +### 为什么不继续沿用 `spec_from_file_location` + +原因如下: + +- 密文插件正文不再是合法 Python 源码 +- 若继续沿用 `importlib` 文件执行,势必要把明文源码先落盘 +- 这会直接削弱“避免用户直接读取源码”的目标 + +因此,对加密插件的正确路径是: + +- 把缓存文件当“文本包” +- 在内存中验签和解密 +- 用 `compile/exec` 创建模块对象 + +## 宿主侧模块结构 + +建议新增如下模块: + +- `atv_player/plugins/spider_crypto/package.py` + - 解析 `secspider/1` + - 校验头字段 + - 输出规范化签名输入 +- `atv_player/plugins/spider_crypto/keyring.py` + - 维护 `kid -> public_key/master_secret` +- `atv_player/plugins/spider_crypto/runtime.py` + - 验签 + - 派生 `wrap_key` + - 解包 `content_key` + - 解密 `payload` + - `compile/exec` 生成内存模块 +- `atv_player/plugins/spider_crypto/errors.py` + - 定义格式、签名、密钥、解密、运行期错误 + +`SpiderPluginLoader` 只负责调度,不直接承载加解密细节。 + +## 运行时序设计 + +密文插件的标准加载时序如下: + +1. 读取插件文件文本 +2. 解析注释头和 `payload.base64` +3. 检查 `format == secspider/1` +4. 按 `kid` 获取签名公钥 +5. 对头字段和 payload 执行 `Ed25519` 验签 +6. 验签通过后,按 `kid/codeID/version` 派生 `wrap_key` +7. 解开 `ek` 得到 `content_key` +8. 使用 `content_key + nonce` 解密 `payload` +9. 对解密后的源码做 `sha256` 校验,必须与 `hash` 一致 +10. 将源码编译为 `code object` +11. 在新的 `types.ModuleType` namespace 中执行 +12. 返回 module 对象给 `SpiderPluginLoader` + +执行时应满足: + +- 明文源码不写回磁盘 +- 模块命名保持宿主当前风格 +- 解密失败和运行失败分开报错 + +## 推荐的运行时接口 + +建议形成如下边界: + +```python +class SecSpiderPackage: + @classmethod + def parse(cls, text: str) -> "SecSpiderPackage": ... + def signing_bytes(self) -> bytes: ... + +class SpiderKeyring: + def get_public_key(self, kid: str): ... + def get_master_secret(self, kid: str) -> bytes: ... + +class SecSpiderRuntime: + def __init__(self, keyring: SpiderKeyring) -> None: ... + def load_module(self, pkg: SecSpiderPackage, module_name: str) -> types.ModuleType: ... +``` + +这样可以把: + +- 包格式解析 +- 密钥管理 +- 运行时加载 + +三个责任分开,避免 `SpiderPluginLoader` 继续膨胀。 + +## 远程下载与缓存策略 + +当前宿主远程插件会下载后缓存到本地文件。此行为可以保留,但缓存内容应始终是“原始加密包文本”,而不是解密后的源码。 + +建议: + +- `_resolve_source_path()` 继续负责下载并落地缓存 +- 缓存文件只用于下次重新加载原始包 +- 每次加载仍重新验签和解密 +- 如需提速,只做内存级缓存,不做明文持久化缓存 + +缓存文件后缀可以考虑从固定 `.py` 调整为更中性的扩展名,例如: + +- `.spkg` +- `.txt` + +若短期不想改后缀,也应在代码语义上把其视为“插件文本缓存”,而不是默认假设为 Python 源码文件。 + +## 构建器设计 + +构建器职责如下: + +1. 读取明文 Spider 源码 +2. 计算源码 `sha256` +3. 生成随机 `content_key` +4. 生成随机 `nonce` +5. 可选压缩源码 +6. 使用 `AES-256-GCM` 加密源码 +7. 基于 `master_secret + kid + codeID + version` 派生 `wrap_key` +8. 用 `wrap_key` 包裹 `content_key` +9. 生成规范化包头 +10. 对头部和 payload 做 `Ed25519` 签名 +11. 输出最终文本封包 + +构建器只在受控环境中使用,不进入宿主。 + +## 错误分类设计 + +宿主侧应把底层异常统一映射为明确的用户可见错误: + +- `插件格式不支持` +- `插件签名校验失败` +- `插件密钥不可用` +- `插件解密失败` +- `插件源码校验失败` +- `插件缺少 Spider 类` +- `插件运行失败: ...` +- `缺少依赖: ...` + +这样做的目的是: + +- 便于日志区分是下载问题、协议问题、签名问题还是运行问题 +- 便于 UI 或调用方给出稳定提示 + +## 兼容与迁移策略 + +推荐分三步迁移: + +### 第一步:宿主支持双栈 + +- 明文插件继续可加载 +- 新增 `secspider/1` 加密插件加载能力 + +### 第二步:单插件试点 + +- 选择一个 Spider 先完成从明文到加密包的发布验证 +- 检查下载、缓存、验签、解密和错误处理链路 + +### 第三步:逐步扩大覆盖面 + +- 将更多 Spider 改为由构建器生成加密包 +- 明文源码只保留在私有开发或构建环境 + +本次设计不要求一次性淘汰全部明文插件。 + +## 测试策略 + +至少覆盖以下测试: + +1. 包解析测试 + - 缺少字段 + - 重复字段 + - 非法 `format` + - 缺失 `payload` +2. 验签测试 + - 改动头字段任一字符后验签失败 + - 改动 payload 任一字符后验签失败 +3. 解密测试 + - 错误 `kid` + - 错误 `ek` + - 错误 `nonce` + - 错误 `hash` +4. 模块加载测试 + - 成功加载后模块含 `Spider` 类 + - `init(config_text)` 能按现有方式调用 +5. 兼容测试 + - 明文插件加载逻辑不回退 +6. 缓存测试 + - 远程下载失败时,若已有有效缓存,仍能回退到缓存包 + +所有测试应使用本地 fixture,不依赖真实网络和真实私钥环境。 + +## 风险与约束 + +- 宿主主密钥可被逆向提取,因此无法防住高级对手 +- 攻击者可以自行修改宿主,去掉验签和解密保护 +- 若未来需要做授权控制,必须引入新的密钥分发或服务端参与机制 +- 若协议字段顺序、空白或换行规则不固定,签名实现很容易出错,因此必须定义严格的 canonicalization 规则 + +## 验收标准 + +- 明文插件加载行为保持兼容 +- `secspider/1` 插件可从本地和远程加载 +- 被篡改的插件会在验签阶段拒绝执行 +- 密钥错误或密文损坏会在解密阶段明确失败 +- 加载成功后,宿主仍像处理普通插件一样处理 `Spider` 实例 +- 解密后的源码不落盘 +- 宿主错误提示可区分格式、签名、密钥、解密和运行期问题