14 KiB
SecSpider 加密插件设计
目标
为当前 Spider 插件体系设计一套可落地的“源码封包”机制,用于隐藏 Spider 明文源码,避免普通用户直接打开插件文件即可看到实现细节。
本次设计面向的宿主前提如下:
- 宿主支持修改加载流程
- 插件既可能来自本地文件,也可能来自远程下载
- 宿主本身是开源可见、运行在用户设备上的 Python 客户端
- 现有 Spider 插件接口保持不变,仍以
Spider类作为宿主入口
本次设计的核心目标是:
- 让发布物不再是直接可读的 Python 明文源码
- 支持宿主对插件做来源校验和完整性校验
- 在不改变 Spider 对外接口的前提下完成加载
- 保持明文插件与加密插件双栈兼容,便于渐进迁移
本次设计不追求:
- 防住能修改宿主、抓内存、下断点的逆向者
- 做在线授权平台
- 做设备绑定、过期控制或远程吊销
- 让用户设备上的插件“不可提取”
背景与边界
当前宿主 SpiderPluginLoader 使用 importlib.util.spec_from_file_location(...) 直接执行本地 Python 文件,并约定模块中导出 Spider 类。该机制适合明文源码插件,但不适合密文封包。
由于本次目标是隐藏源码而不是彻底防逆向,设计必须先承认以下边界:
- 只要插件最终要在本地运行,明文源码或等价字节码一定会在某个时刻进入解释器
- 解密端开源且跑在用户设备上,因此任何宿主内置密钥都可能被逆向提取
- 这意味着方案的安全收益是“提高获取源码的门槛”,而不是“保证源码永不泄露”
因此,本次设计定位为:
- 源码封包机制
- 完整性与签名校验机制
- 宿主侧双栈插件加载机制
而不是 DRM 或不可导出的执行环境。
方案选择
候选方案:
- 纯混淆方案
- 离线封包 + 本地验签 + 本地解密方案
- 在线授权下发解密材料方案
选用方案 2。
原因:
- 用户当前目标只是避免普通用户直接看源码,不需要引入在线依赖
- 宿主已允许修改,适合增加自定义 loader
- 本地验签可以防止第三方伪造或篡改插件
- 双栈方案能平滑兼容现有明文插件生态
- 相比在线授权,工程复杂度和运维成本更可控
不选方案 1 的原因:
- 只能防止双击直读,无法提供可靠的篡改检测
- 无法建立统一的协议版本、密钥轮换和签名链路
不选方案 3 的原因:
- 超出当前目标
- 需要额外服务端、授权链路和可用性保障
总体架构
系统分成三个角色:
builder- 读取明文 Spider 源码
- 生成内容密钥
- 加密源码
- 生成签名
- 输出单文件加密包
loader- 读取插件文件
- 识别明文或密文格式
- 对密文包执行验签、解密、内存加载
runtime spider- 解密后在内存中编译执行
- 对宿主继续暴露
Spider类
总体链路如下:
- 构建阶段读取明文 Spider 源码
- 生成随机内容密钥
content_key - 用
content_key对源码做对称加密 - 使用宿主密钥派生链路包裹
content_key - 对包头和密文内容做签名
- 产出单文件文本封包
- 宿主加载时先识别格式
- 若为明文插件,则继续按现有
importlib逻辑执行 - 若为密文插件,则先验签,再解包内容密钥,再解密源码
- 解密后的源码通过
compile/exec在内存中执行 - 宿主统一从模块中读取
Spider类并实例化
封包格式设计
建议定义文本协议 secspider/1,保持现有注释头风格,便于远程分发和人工排查。
推荐格式:
// 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:...
字段分组如下:
- 业务元数据
nameversionremark
- 协议元数据
formatalgwrapsignkid
- 加密与校验字段
nonceekhashsig
- 数据字段
payload.base64
字段约束如下:
name、version、remark是仅保留的业务元数据字段format固定为secspider/1alg首版固定为aes-256-gcmwrap首版固定为hkdf-aes-keywrapsign首版固定为ed25519kid表示当前包使用的密钥版本nonce为内容加密随机数ek为包裹后的内容密钥,不是主密钥本身hash为解密前明文源码的sha256sig覆盖除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
用途:
- 只用于当前插件包的源码加密
优势:
- 每个插件包独立加密
- 不同包之间不共享实际内容密钥
密钥派生规则
建议:
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 的核心流程为:
- 安装兼容
base.spider - 通过本地路径或远程下载得到插件文件
- 通过
importlib.util.spec_from_file_location(...)执行源码文件 - 从模块中读取
Spider类并实例化
这条链路仅适合明文 Python 文件。
目标形态
宿主改造为双栈加载:
- 明文插件:保持现有
importlib路径 - 加密插件:走自定义
secspiderruntime 路径
两条路径最终都返回 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
主流程调整为:
_install_compat_modules()_resolve_source_path()_detect_package_format(source_path)- 如果不是
secspider/1,则走_load_plain_module(...) - 如果是
secspider/1,则走_load_secspider_module(...) - 统一读取
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 只负责调度,不直接承载加解密细节。
运行时序设计
密文插件的标准加载时序如下:
- 读取插件文件文本
- 解析注释头和
payload.base64 - 检查
format == secspider/1 - 按
kid获取签名公钥 - 对头字段和 payload 执行
Ed25519验签 - 验签通过后,按
kid/name/version派生wrap_key - 解开
ek得到content_key - 使用
content_key + nonce解密payload - 对解密后的源码做
sha256校验,必须与hash一致 - 将源码编译为
code object - 在新的
types.ModuleTypenamespace 中执行 - 返回 module 对象给
SpiderPluginLoader
执行时应满足:
- 明文源码不写回磁盘
- 模块命名保持宿主当前风格
- 解密失败和运行失败分开报错
推荐的运行时接口
建议形成如下边界:
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 源码文件。
构建器设计
构建器职责如下:
- 读取明文 Spider 源码
- 计算源码
sha256 - 生成随机
content_key - 生成随机
nonce - 可选压缩源码
- 使用
AES-256-GCM加密源码 - 基于
master_secret + kid + name + version派生wrap_key - 用
wrap_key包裹content_key - 生成规范化包头
- 对头部和 payload 做
Ed25519签名 - 输出最终文本封包
构建器只在受控环境中使用,不进入宿主。
错误分类设计
宿主侧应把底层异常统一映射为明确的用户可见错误:
插件格式不支持插件签名校验失败插件密钥不可用插件解密失败插件源码校验失败插件缺少 Spider 类插件运行失败: ...缺少依赖: ...
这样做的目的是:
- 便于日志区分是下载问题、协议问题、签名问题还是运行问题
- 便于 UI 或调用方给出稳定提示
兼容与迁移策略
推荐分三步迁移:
第一步:宿主支持双栈
- 明文插件继续可加载
- 新增
secspider/1加密插件加载能力
第二步:单插件试点
- 选择一个 Spider 先完成从明文到加密包的发布验证
- 检查下载、缓存、验签、解密和错误处理链路
第三步:逐步扩大覆盖面
- 将更多 Spider 改为由构建器生成加密包
- 明文源码只保留在私有开发或构建环境
本次设计不要求一次性淘汰全部明文插件。
测试策略
至少覆盖以下测试:
- 包解析测试
- 缺少字段
- 重复字段
- 非法
format - 缺失
payload
- 验签测试
- 改动头字段任一字符后验签失败
- 改动 payload 任一字符后验签失败
- 解密测试
- 错误
kid - 错误
ek - 错误
nonce - 错误
hash
- 错误
- 模块加载测试
- 成功加载后模块含
Spider类 init(config_text)能按现有方式调用
- 成功加载后模块含
- 兼容测试
- 明文插件加载逻辑不回退
- 缓存测试
- 远程下载失败时,若已有有效缓存,仍能回退到缓存包
所有测试应使用本地 fixture,不依赖真实网络和真实私钥环境。
风险与约束
- 宿主主密钥可被逆向提取,因此无法防住高级对手
- 攻击者可以自行修改宿主,去掉验签和解密保护
- 若未来需要做授权控制,必须引入新的密钥分发或服务端参与机制
- 若协议字段顺序、空白或换行规则不固定,签名实现很容易出错,因此必须定义严格的 canonicalization 规则
验收标准
- 明文插件加载行为保持兼容
secspider/1插件可从本地和远程加载- 被篡改的插件会在验签阶段拒绝执行
- 密钥错误或密文损坏会在解密阶段明确失败
- 加载成功后,宿主仍像处理普通插件一样处理
Spider实例 - 解密后的源码不落盘
- 宿主错误提示可区分格式、签名、密钥、解密和运行期问题