Files
ShadowsocksX-NG/docs/adr/0035-encrypt-persistent-server-library.md
Qiu Yuzhou f9e95474e5 refactor: adopt age configuration storage
Replace the custom encrypted container with AgeSwift 0.1.0 so standard
age tools can inspect sensitive configuration independently of the app.

Use independent Keychain X25519 identities for catalog.age and
proxy-listening.age. Preserve atomic commits and confirmed recovery;
reject historical NG2 formats without migration or cleanup.
2026-10-21 14:31:32 +08:00

8.6 KiB
Raw Permalink Blame History

持久服务器库整体加密与主密钥恢复边界

后续修订:加密格式、文件名、identity 存储和匹配方式按 ADR-0050 执行;范围外的监听配置采用独立主密钥整体加密,见 ADR-0042;该修订已实施,服务器库自身的恢复规则保持不变。

Status: Accepted(2026-10-18)。

实施状态:整体加密、记录直接携带秘密及移除 NG2 历史格式兼容均已实施。当前实现以代码为准,验证结果见对应交付记录。

持久服务器库采用标准 age 文件格式整体加密,通过 AgeSwift 使用 X25519 identity,详见 ADR-0050。密码、完整插件参数、订阅 URL 与配置目录、订阅关联、收藏共同进入密文;服务器库专用 Keychain 项只保存标准 age identity 私钥文本,不再逐项保存这些凭据。监听认证等范围外凭据保持独立存储;测试 URL 按 ADR-0036 保存为非敏感偏好。sslocal-active.json 与派生认证文件的加密另案处理。

主密钥采用经典登录钥匙串,并以严格 ACL 限制获准应用访问。该选择优先考虑开源分发中的维护者签名变更和用户自行编译;不通过放宽到任意应用访问来换取兼容。更换签名后的前台重新授权与迁移路径仍须设计,并在真实签名构建中验证,不能承诺静默读取旧密钥。

启动和后台读取不得主动触发授权弹窗;遇到需要交互的状态时显示可重试的错误,只有用户主动解锁时请求系统授权。当前进程成功读取过密钥后继续使用内存资料。签名变更后的重新授权沿用该显式入口,实际系统授权结果以真机验证为准。

首版不提供完整服务器库的独立密码加密备份。用户接受主密钥丢失后通过重新导入或订阅恢复资料;未另存的手动资料无法保证恢复。ADR-0018 的 SIP-008 明文配置组导出保持其原有用途,不等同于完整服务器库的加密备份。

内存与加载失败

应用成功解密后,主密钥与服务器库明文在当前应用进程内保留至退出;首版不增加手动锁定功能。此后锁定钥匙串不撤销当前进程已经读取的资料,当前应用继续工作。应用重新启动后重新读取主密钥。

整体解密使服务器密码随服务器库进入内存,因此调整 ADR-0030 的按需读取边界:不再承诺只有打开编辑界面才装载密码,也不再承诺插件参数明文只在详情会话中持有。详情界面仍不显示密码,编辑草稿仍与详情展示分离。非秘密展示投影、诊断与日志不得因整体解密而包含秘密。

加载失败时保留原文件,阻止服务器库写入,并提供重试入口;已有后台代理服务继续运行。不得将解密失败、密钥不可用、格式不支持或文件损坏自动视为空库。加载失败后,只有用户明确确认放弃原配置时,才进入新建流程。

用户可以在加载失败后明确确认创建空库,并覆盖旧文件。该操作必须独立于重试,确认文案必须说明覆盖将放弃旧服务器库;取消操作不得修改旧文件。此授权是产品行为决定,不是本次设计讨论中立即删除本机数据的授权。重置时创建新主密钥,成功保存新空库后清除活动目标并撤下原服务器出口,保留代理服务开关意图,沿用现有空库收敛行为。新空库保存失败时保留旧文件与原运行状态。

加载失败状态不得作为空库传入启动时的活动目标重展开流程。当前活动目标独立持久化;以空库重展开会清除目标并部署无服务器出口的运行配置,违背失败时保留已有代理运行的决定。加载失败状态必须与真正空库明确区分。

记录与提交

服务器记录直接保存密码与可选的完整插件参数,订阅记录直接保存订阅 URL。不再保留库内凭据引用、独立秘密字典或按引用读取秘密的存储。主密钥仍独立保存在 Keychain;范围外凭据存储不因此改变。

目录、订阅与收藏组成完整工作副本,秘密随所属记录一起修改。提交执行「修改副本 → 校验 → 整体加密并原子保存 → 发布完整已提交值」。失败时丢弃副本,保留旧字段与旧秘密,不再为服务器库执行逐项凭据补偿。运行时收敛仍在持久化成功后进行,运行时失败不回滚已提交服务器库。

复制记录时复制凭据值并生成新节点身份,后续修改互不影响;订阅刷新保留既定节点身份与顺序规则,不再复用或清理凭据引用。订阅 URL 编辑必须先持久化成功再请求刷新,且不因 URL 编辑本身重展开服务器出口。

生产服务器库存储只提供加密写入,移除未注入加密存储时的明文 JSON 写入路径。显式配置组导出与分享保持原有用途;非秘密展示投影、诊断与日志不得直接携带完整记录。

历史格式与旧版导入

NG2 尚未发版,不再支持其历史数据迁移,包括旧 catalog.json 与逐项 Keychain 凭据,以及已实现的「目录 + 引用秘密表」加密格式。新记录格式使用明确的新版本,只读取当前格式;移除旧版本兼容解码、自动迁移与迁移清理,不读取或清理旧逐项 Keychain 凭据。

持久文件路径按 ADR-0050 改为 catalog.age。发现旧自研 catalog.enc 或旧格式库时报告不支持的格式,阻止写入,不自动转换、不退回其他旧文件,也不静默当作新安装。只有当前库与历史 .enc、.json 均不存在时才按全新空库处理;旧 catalog.json 的存在检查只用于识别旧库,不读取其内容。用户仍可明确确认放弃旧库并创建空库;该操作只提交新的 catalog.age,不增加对旧文件或旧 Keychain 项的清理。

保留用户主动执行的旧版 ShadowsocksX-NG 服务器导入与再次导入。这是 ADR-0002 确定的产品功能,不属于 NG2 内部格式迁移。导入直接构造携带凭据值的手动记录,保留既定身份、完成标记和失败恢复语义;不导入旧版偏好或改写旧版数据。

主密钥生命周期

首版不提供手动或定期轮换入口。正常保存沿用匹配的 identity,每次生成新的 age 密文;用户明确重置服务器库时创建新主密钥。密钥创建与首份文件提交的失败保护属于实现契约,不得先删除旧密钥再尝试写入新文件。

实施与验收

实现保持单应用进程、串行保存与单个已匹配 identity 的内存模型;Keychain 保留重置前的 identity,不引入多进程迁移协调、自动轮换或历史密文恢复系统。采用标准 age v1,不附加自研文件头;JSON 结构版本在密文内,读取兼容二进制和 ASCII armor。服务器库保存过程只写密文临时文件,不向日志或诊断报告增加秘密。主密钥不写入普通文件、日志或诊断报告;运行契约沿用本文开头规定的范围。磁盘权限沿用目录 0700、文件 0600。

验收集中于本次实际改变的路径:

  • 文件与加密:完整资料往返不丢字段;多次保存生成不同密文;错误密钥、篡改与不支持版本返回错误;文件不含样本资料明文。
  • 格式与失败:历史明文库与旧加密格式均报告不支持,不转换、不覆盖、不清理旧凭据;提交失败保留旧文件和完整已提交内存值;只有明确确认重置才创建空库。
  • 记录与提交:创建、编辑、导入与订阅刷新同时提交字段和秘密;复制后凭据修改互不影响;订阅 URL 保存失败不请求刷新;新存储不存在生产明文写入路径;保留诊断与展示投影脱敏。
  • 产品行为:加载失败不写空库、不重展开原活动目标;授权仅由用户主动触发;取消重置保留旧数据,确认且保存成功后清除目标;现有导入、编辑、订阅与激活兼容。
  • 真机:验证实际构建下的主密钥保存和读取、退出重启、钥匙串锁定与前台授权;运行进程验证与纯文件测试分别记录。没有实际执行的签名身份变更或跨机恢复测试标为未验证,不扩展为首版完整签名组合测试矩阵。

用户通过 implement 指令确认实施本次修订。上述验收条件是实施检查清单,实际执行结果见对应提交与交付记录。

研究依据见 服务器配置文件整体加密方案研究。该报告是候选方案与调研记录,本 ADR 仅采纳上述明确列出的决定。