# 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 //@version:1 //@remark: //@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` - `version` - `remark` - 协议元数据 - `format` - `alg` - `wrap` - `sign` - `kid` - 加密与校验字段 - `nonce` - `ek` - `hash` - `sig` - 数据字段 - `payload.base64` 字段约束如下: - `name`、`version`、`remark` 是仅保留的业务元数据字段 - `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/name/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:" + name + ":" + version ) ``` 用途: - `wrap_key` 用于包裹或解包 `content_key` 收益: - 同一 `master_secret` 下,不同 `name/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/name/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 + name + 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` 实例 - 解密后的源码不落盘 - 宿主错误提示可区分格式、签名、密钥、解密和运行期问题