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));
  ...
}

三个关键事实:

  1. 兜底就是出厂默认。桌面端创建加密器是无参调用,启动链路没有任何地方注入 ZCODE_CREDENTIAL_SECRET(不读钥匙串、不生成随机密钥、没有设置页入口)。官方 3.14.1 发行包解包验证:该变量全包只出现在"读取环境变量"那一处,无任何赋值。
  2. 文档不提。README 环境变量表没有它;NOTICE.md 仅一句"默认密钥可从本机环境信息派生"。
  3. 没有 KDF、没有盐。即便用户名未知,sha256 单轮哈希也允许每秒数百万次猜测,GCM auth tag 还提供了免费的"猜对与否"判定信号。

攻击面

场景 攻击者需要什么 成本
同机恶意软件 / 任意低权限进程 能读 ~/.zcode/(同用户权限即可,无需提权) 重算一次哈希
备份 / 网盘 / Time Machine 泄露 密文文件 + 路径里的用户名 同上,跨机器可解
二手设备 / 磁盘镜像未清干净 拿到磁盘即可 同上

密钥不含任何硬件因子,密文跨机器可解密。被盗对象是用户存在里面的全部模型 API Key 与 Coding Plan token。

对照组:macOS 本来就有 Keychain(硬件级、按 app 授权、绑定登录密码)。NOTICE.md 自己承认"并非系统钥匙串"——不是做不到,是没做。

一句话结论

对一个替用户保管多家模型厂商凭据的 AI 编程工具,默认密钥管理等效于明文存储:加密只是把"读文件"换成了"读文件 + 算一次 sha256"。


本文仅陈述可在公开源码中复核的事实,供安全评估与选型参考。

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论