bug: 自定义供应商旧密钥密文不可解密时配置更新死锁,新 API key 也无法保存
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 401
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
### 问题描述 / What happened
自定义供应商已存的 API key 密文一旦用当前 safeStorage 密钥解不开(例如 Keychain 密钥变更、或密文由另一个构建写入),该供应商的配置更新会**永久死锁**:即使用户在表单里粘贴了全新的 API key 点保存,也始终报 `failed to read existing provider credential`,没有任何自救路径(UI 上无法清除坏密文,只能手动去 `safe-storage/` 目录删 `.enc` 文件)。
根因链路:
- `maker:provider:custom:update` 在写入新 key 前,会先经 `stageProviderKeys` 对每个待变更 runtime 做严格快照读取([providerHandlers.ts#L677-L680](https://github.com/makecindy/cindy/blob/11e02c0306f86fc1077b8eb74580a161423780ce/apps/desktop/src/main/maker-ipc/providerHandlers.ts#L677-L680))。
- 快照读取走 [`readCustomProviderKeyForMutation`](https://github.com/makecindy/cindy/blob/11e02c0306f86fc1077b8eb74580a161423780ce/apps/desktop/src/main/secrets/providerSecretStore.ts#L442),按注释设计「仅 ENOENT 表示没有旧 key,解密失败必须抛错」,目的是防止把暂时不可读的凭证误判为空而永久删除。
- 但当密文是**永久性**不可解(safeStorage 密钥已变,重试多少次都一样)时,这层保护反而把用户锁死:明明提供了替换值,旧值反正会被覆盖,却因为「拿不到回滚快照」拒绝整个保存。`readCustomProviderHeadersForMutation` 的请求头路径同理([providerHandlers.ts#L773](https://github.com/makecindy/cindy/blob/11e02c0306f86fc1077b8eb74580a161423780ce/apps/desktop/src/main/maker-ipc/providerHandlers.ts#L773))。
期望行为(建议方向,供维护者裁量):
1. 当本次 mutation 携带**显式替换值**时,快照解密失败可降级为「快照 = 不可恢复」,回滚语义退化为删除该 runtime 的密文,不阻塞覆盖写入;仅在 mutation 是「保留旧值」或「仅删除」语义时维持现有严格失败。
2. 或者至少:错误信息告知用户旧凭证已损坏及恢复方式(重新填 key / 提供「清除已存凭证」入口),而不是笼统的 INTERNAL。
### 环境 / Environment
- Cindy 版本或 commit / version or commit: 0.1.71(release 安装版);main `11e02c03` 上代码路径一致
- 平台与版本 / platform & OS version: macOS (Darwin 25.4.0, Apple Silicon)
- 安装方式 / install method: 官方 dmg 安装到 /Applications
### 复现步骤 / Steps to reproduce
1. 自定义供应商配置好 API key(密文落盘为 `userData/safe-storage/owner__provider_key__.enc`)。
2. 使该密文对当前 safeStorage 密钥不可解。实机是历史写入的密文解不开(疑似曾由不同签名/构建写入);模拟复现可直接用任意 base64 垃圾字节覆盖该 `.enc` 文件内容。
3. 打开「编辑自定义供应商」,粘贴一个全新的 API key,点保存。
4. 观察:顶部 toast 报 `failed to read existing claude-code provider credential`,保存失败;反复重填 key 均无效,UI 无任何恢复路径。
5. 手动把对应 `.enc` 文件移走后再保存 → 立即成功(证明死锁仅由坏密文快照引起)。
### 日志与截图 / Logs & screenshots
```
[2026-09-02T22:36:34.282+08:00] [WARN ] [providerSecretStore] {
providerId: 'kimi-code',
agent: 'claude-code',
err: 'Error while decrypting the ciphertext provided to safeStorage.decryptString.'
} decrypt custom provider key snapshot failed
[2026-09-02T22:36:34.282+08:00] [ERROR] [console] Error occurred in handler for 'maker:provider:custom:update': Error: [INTERNAL] failed to read existing claude-code provider credential
at createIpcError (.../bootstrap-electron-ClZHv728.js:221:19129)
at throwIpcError (.../bootstrap-electron-ClZHv728.js:221:19279)
```
(已按模板要求删除凭证与个人数据;`kimi-code` 为本机自定义供应商 id。)
Contributor guide
Research direction
Start in apps/desktop/src/main/maker-ipc/providerHandlers.ts at stageProviderKeys and the provider headers path, then inspect readCustomProviderKeyForMutation and readCustomProviderHeadersForMutation in providerSecretStore.ts. Reproduce with an unreadable .enc file and a replacement key. Done means an explicit replacement can be saved despite an unrecoverable old ciphertext, while retain-old and delete-only mutations keep their existing strict behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- desktop, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100