Files
ShadowsocksX-NG/docs/server-library-encryption-format.zh-CN.md
T
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

6.5 KiB
Raw Blame History

使用 age 加密敏感配置

English | 简体中文

服务器库和代理监听配置通过 AgeSwift 使用标准 age v1 文件格式。采用理由与恢复边界见 ADR-0050。

文件与载荷

两个文件均位于 ~/Library/Application Support/ShadowsocksX-NG2/configuration/:

文件 解密后的 UTF-8 JSON 明文大小上限
catalog.age 目录 schema v7:version、rootChildren、entries、subscriptions、favoriteIDs 64 MiB
proxy-listening.age { "version": 1, "configuration": { "local": …, "lan": … } } 1 MiB

服务器库包含完整配置目录、订阅与有序收藏;服务器密码、完整插件参数、订阅 URL 直接保存在所属记录中。监听认证资料属于独立监听配置。运行 JSON、LAN 认证文件、用户主动导出与分享不属于本格式的范围。

保存使用单个 X25519 recipient 的二进制 age v1,不增加应用文件头、UUID 前缀、旁路索引、外层 Base64 或自研密码容器。读取也接受标准 ASCII armor;再次保存统一写二进制。JSON 结构版本位于加密载荷中,与 age 格式版本独立。完整模型通过校验后才发布。

空服务器库的载荷为:

{
  "entries": [],
  "favoriteIDs": [],
  "rootChildren": [],
  "subscriptions": [],
  "version": 7
}

目录层级与顺序以 rootChildren 和分组子节点数组为准。秘密是记录中的普通值,不是凭据引用。完整结构以 Codable 模型为准,空白与对象键顺序不影响语义。

Keychain identity

两个存储分别使用经典登录钥匙串中的 Generic Password service:

文件 Service
catalog.age com.qiuyuzhou.ShadowsocksX-NG2.server-library.age
proxy-listening.age com.qiuyuzhou.ShadowsocksX-NG2.proxy-listening.age

每条记录的 account 是 UUID,值为 UTF-8 编码的标准 AGE-SECRET-KEY-1... identity 文本,不是 AES 原始字节。记录不同步、不使用 Data Protection Keychain;创建时通过经典 Keychain ACL 仅授权当前应用。

文件不包含 Keychain account 定位信息。读取器枚举对应 service 的 account,尝试 identity 直至成功。存在无法访问或不匹配的候选时,其他可访问且匹配的 identity 仍能正常解密。正常保存复用匹配的 identity,并在当前进程缓存。确认重置时,先保存并回读新 identity,再原子替换密文;保留旧 identity,使提交失败或进程中断时旧文件仍可读取。记录随重置次数增加,不提供自动清理或手动轮换。

启动和后台读取不请求系统授权弹窗,继续沿用既有显式交互入口。丢失匹配 identity 后无法解密;更换格式不绕过 Keychain 授权,也不提供独立恢复能力。

使用标准工具检查

通过 brew install age 安装参考 CLI。在“钥匙串访问”中搜索上表完整 service,查看 account UUID。使用系统 security 工具,经系统授权后取得 identity。将 ACCOUNT_UUID 替换为实际账户;有多条记录时,可重复执行导出命令,把其他 identity 追加到同一个文件。

umask 077
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
security find-generic-password \
  -s com.qiuyuzhou.ShadowsocksX-NG2.server-library.age \
  -a ACCOUNT_UUID -w >> "$workdir/identities.txt"
age --decrypt -i "$workdir/identities.txt" \
  "$HOME/Library/Application Support/ShadowsocksX-NG2/configuration/catalog.age" | jq .

检查监听配置时,替换 service 和文件名为监听对应值。系统提示时选择单次授权,不放宽应用 ACL。identity 文件属于秘密,输出包含配置凭据。这些命令是维护者主动导出临时 identity 的调试操作;应用自身不向普通文件、日志或诊断报告写出 identity。删除临时文件不代表安全擦除。

标准 age 工具也能用对应的 age1... recipient 加密 JSON;应用仍会检查结构和大小。手动替换持久配置前退出 GUI,避免运行中的应用使用内存已提交值覆盖人工修改。无需项目专用解密器或应用内私钥导出界面。

读取、提交与恢复

读取前限制文件大小,计入 age 分块标签、文件头余量和 armor 开销;解密后再次检查明文上限。age 格式错误、identity 不可用、认证失败或 JSON 校验失败都会导致加载失败,不回退明文,不自动用默认值覆盖。

保存先编码并校验完整值,再在内存加密,使用同目录的纯密文临时文件原子替换目标。目录权限为 0700,临时文件和目标文件为 0600。当前写入器不显式调用 fsync,原子替换不保证断电持久性。成功保存后才发布应用内存状态。

新 .age 文件存在时优先使用它;不存在但对应 .enc 或历史 .json 存在时,报告不支持的格式,不按全新安装初始化。不迁移、读取、删除或回退到历史 NG2 文件,不访问或清理旧 AES 与逐项 Keychain 凭据。用户确认重置后创建新的 .age 文件,旧文件保持原样。

服务器库保留重试、显式解锁和确认创建空库;监听配置保留启动门禁和确认重置,默认关闭局域网监听。加载失败或重置失败期间保留原文件、已有后台代理与系统代理。用户主动执行的旧版 ShadowsocksX-NG 导入仍保留。

验证与限制

AgeConfigurationStorageTests 覆盖两份配置与标准 age CLI 的二进制、armor 双向互操作,以及旧文件拒绝、候选 identity 匹配、重置提交失败、结构和大小检查、隔离的真实 Keychain 操作。运行该测试需要 PATH 或 Homebrew bin 目录内存在 age,并能够访问登录钥匙串。

age 保护静态内容,文件大小与时间戳仍可见。它不提供回滚保护、安全内存擦除、跨机 identity 恢复或独立密码加密备份。应用持有已解密资料和匹配 identity 至退出。

源码入口:EncryptedFileStorage、MasterKeyStore、CatalogFileStore、ProxyListeningFileStore、AtomicFileWriter。