ZCode (zai-org/ZCode) 凭据加密的兜底密钥机制:默认情况下加密密钥由平台/家目录/用户名推导,偷取密文文件即等于拿到全部 API Key
ZCode 凭据加密的兜底密钥:把"偷 API Key"变成了纯算术题
基于 zai-org/ZCode "开源"仓库(commit 872ad96)与官方 3.14.1 发行包(mac-arm64)的解包对比。所有代码路径均可在开源仓库中复核。 TL;DR
ZCode 把用户的各模型厂商 API Key(含 OAuth token)用 AES-256-GCM 加密后落盘。但默认情况下加密密钥不是任何秘密,而是由平台名 + 家目录路径 + OS 用户名拼接的字符串做一次 SHA-256:
key = sha256("zcode-credential-fallback:darwin:/Users/<用户名>:<用户名>")
而密文文件本身就存在 ~/.zcode/ 下——文件属主就是用户名。攻击者需要的全部信息与密文物理上放在一起。不存在爆破,只存在"重算一次哈希",实测不到 1 毫秒。
代码链路
packages/services/src/credential/providers/credentialCipherProvider.ts(桌面端,CLI 侧 apps/zcode-cli/packages/adapters/src/auth/credential-cipher.ts 同构):
function deriveCipherKey(secret: string): Buffer {
return createHash("sha256").update(secret).digest(); // 单轮、无盐、无 KDF
}
function defaultCredentialSecret(env: NodeJS.ProcessEnv): string {
const configuredSecret = env["ZCODE_CREDENTIAL_SECRET"];
if (configuredSecret) return configuredSecret; // 唯一的非兜底出口
return `zcode-credential-fallback:${platform()}:${homedir()}:${username}`;
}
export function createCredentialCipherProvider(options = {}) {
const env = options.env ?? process.env; // 桌面端为无参调用,必然走 process.env
const key = deriveCipherKey(defaultCredentialSecret(env));
...
}三个关键事实:
- 兜底就是出厂默认。桌面端创建加密器是无参调用,启动链路没有任何地方注入
ZCODE_CREDENTIAL_SECRET(不读钥匙串、不生成随机密钥、没有设置页入口)。官方 3.14.1 发行包解包验证:该变量全包只出现在"读取环境变量"那一处,无任何赋值。 - 文档不提。README 环境变量表没有它;NOTICE.md 仅一句"默认密钥可从本机环境信息派生"。
- 没有 KDF、没有盐。即便用户名未知,sha256 单轮哈希也允许每秒数百万次猜测,GCM auth tag 还提供了免费的"猜对与否"判定信号。
攻击面
| 场景 | 攻击者需要什么 | 成本 |
|---|---|---|
| 同机恶意软件 / 任意低权限进程 | 能读 ~/.zcode/(同用户权限即可,无需提权) | 重算一次哈希 |
| 备份 / 网盘 / Time Machine 泄露 | 密文文件 + 路径里的用户名 | 同上,跨机器可解 |
| 二手设备 / 磁盘镜像未清干净 | 拿到磁盘即可 | 同上 |
密钥不含任何硬件因子,密文跨机器可解密。被盗对象是用户存在里面的全部模型 API Key 与 Coding Plan token。
对照组:macOS 本来就有 Keychain(硬件级、按 app 授权、绑定登录密码)。NOTICE.md 自己承认"并非系统钥匙串"——不是做不到,是没做。
一句话结论
对一个替用户保管多家模型厂商凭据的 AI 编程工具,默认密钥管理等效于明文存储:加密只是把"读文件"换成了"读文件 + 算一次 sha256"。
本文仅陈述可在公开源码中复核的事实,供安全评估与选型参考。
评论
?
参与讨论