Files
ShadowsocksX-NG/docs/adr/0050-age-encrypted-configuration-storage.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

5.8 KiB

使用 age 标准格式保存敏感配置

状态:已接受并实施(2026-10-21);用户通过 implement 指令确认实施。验证结果见交付记录。

敏感配置的加密存储采用标准 age 文件格式,使用 AgeSwift 实现。目的是减少自研加密协议与文件容器的维护成本,让维护者能够使用标准 age 兼容命令行工具解密检查配置,并使配置文件长期独立于特定语言、实现与维护者。此选择的主要目标不是提高现有方案的密码学强度。

该决定修订 ADR-0035 与 ADR-0042 中涉及的加密方法和文件封装规定。当前行为仍以代码为准。

已确认的范围与兼容边界

服务器库和代理监听配置同时替换自研加密格式。运行配置与 LAN 认证文件不在本次范围内,其加密另案处理。

延续 NG2 尚未发版、不迁移历史格式的决定。现有自研加密文件属于不支持的历史格式:保留原文件并阻止普通写入,不自动转换,不静默按新安装处理。沿用各自既有的用户明确确认重置流程;未确认重置不覆盖数据。此决定不授权在设计讨论期间重置本机配置或 Keychain 项。

已确认的格式与密钥

直接保存标准二进制 age 文件,不在 age 文件外增加自定义文件头。服务器库文件名改为 catalog.age,代理监听配置文件名改为 proxy-listening.age;配置结构版本放在加密后的 JSON 内。检测到旧 .enc 文件时,仍按不支持的历史格式处理,不因新路径不存在而创建空配置。

两个存储分别生成独立的 X25519 identity,将标准 AGE-SECRET-KEY-1... 私钥文本保存在经典登录钥匙串中,继续限制获准应用访问。正常保存复用 identity,用户明确确认重置时生成新的 identity。

按存储分别建立新的 age Keychain 命名空间,读取其中的 identity,由 AgeSwift 尝试匹配解密,不增加密钥索引文件。成功加载后缓存匹配的 identity,正常保存继续使用它。重置时先保存并回读新 identity,再原子替换密文,保留旧 identity;写盘失败或中途退出时,旧文件仍能用旧 identity 解密。密钥记录随重置次数增加,接受相应读取成本。

保留既有加载失败后的用户确认重置入口,涵盖文件损坏、格式不支持和密钥不可用等情况,不收窄为仅 identity 丢失才能重置。加载失败不自动判定 identity 丢失,也不自动重置。

标准工具与可读格式

开发文档说明如何使用系统 Keychain 工具取得 identity,再使用标准 age CLI 解密;不新增应用内私钥导出界面。保留系统授权要求,私钥不进入日志或诊断报告。

应用接受二进制与 ASCII armor 两种标准 age 文件,保存时统一写二进制。解密仍使用对应存储的 identity,解密后的 JSON 必须通过原有结构与内容校验。不增加口令加密、SSH 密钥或多接收方管理功能。

沿用的行为与失败保护

两个存储继续拥有独立的密钥与恢复生命周期:服务器库加载失败时保留原文件、阻止写入并提供重试或确认创建空库;监听配置加载失败时阻断正常工作区启动,用户确认重置成功后才解除阻断。失败或等待确认期间,保留既有后台代理和系统代理;成功保存后再按原有规则收敛运行状态。

保留后台读取不触发 Keychain 授权弹窗、显式交互入口控制授权、成功读取后在当前进程缓存的规则。Keychain 某个候选 identity 不可读取时,不将其视为文件损坏或自动新建密钥;能否用其他可访问候选正常读取,应以实际解密结果为准。没有可用匹配 identity 时报告读取失败并保留文件。

保留完整值校验、串行保存、仅写密文临时文件、同目录原子替换和保存成功后才发布内存值的契约。目录权限仍为 0700,文件为 0600。服务器库和监听配置的明文大小上限分别保持 64 MiB 和 1 MiB,读取前限制密文大小并考虑 age 分块与 armor 开销,解密后再次检查明文大小。AgeSwift 提供内存加解密 API,不承担文件提交、Keychain 或大小限制。

不迁移、读取或清理旧 AES 密钥记录;不增加自动密钥清理、轮换、完整库加密备份或手动锁定功能。用户主动执行的旧版 ShadowsocksX-NG 导入与现有配置组导出、分享保持既有用途。

实施与验收要求

通过 Swift Package Manager 精确固定 AgeSwift 0.1.0;其 Swift API 底层使用 Rust age 与预编译 XCFramework。实际接入时核实发布产物、校验和及应用和测试目标的链接,不以本地同名仓库作为交付依赖。移除生产自研 AES 文件封装读写,更新相关格式文档及 ADR-0035、ADR-0042 的现行描述,历史研究报告保留原时点内容。

验收覆盖两个存储的完整字段往返、重复保存密文不同、错误或缺失 identity、篡改与截断、大小限制、JSON 结构版本与内容校验,以及二进制和 armor 读取。使用标准 age CLI 与应用实现做双向互操作验证,不仅验证库内自行加解密。

验证旧 .enc 和旧 JSON 的存在不会触发静默初始化;新 .age 存在时以新文件为准,不回退到旧文件。验证提交失败与重置中断保留旧文件和旧 identity 的可读性、两个存储恢复互不影响,以及启动恢复门禁。真机核实 Keychain ACL、后台非交互读取、显式授权和退出重启后的读取,并验证文档中的系统 Keychain 工具与 age CLI 操作;未执行的验证如实标为未验证。